View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Set;
25  
26  import org.antlr.v4.runtime.BufferedTokenStream;
27  import org.antlr.v4.runtime.CommonTokenStream;
28  import org.antlr.v4.runtime.ParserRuleContext;
29  import org.antlr.v4.runtime.Token;
30  import org.antlr.v4.runtime.tree.ParseTree;
31  import org.antlr.v4.runtime.tree.TerminalNode;
32  
33  import com.puppycrawl.tools.checkstyle.api.DetailNode;
34  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
35  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
36  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsLexer;
37  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParser;
38  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParserBaseVisitor;
39  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
40  
41  /**
42   * Visitor class used to build Checkstyle's Javadoc AST from the parse tree
43   * produced by {@link JavadocCommentsParser}. Each overridden {@code visit...}
44   * method visits children of a parse tree node (subrules) or creates terminal
45   * nodes (tokens), and returns a {@link JavadocNodeImpl} subtree as the result.
46   *
47   * <p>
48   * The order of {@code visit...} methods in {@code JavaAstVisitor.java} and production rules in
49   * {@code JavaLanguageParser.g4} should be consistent to ease maintenance.
50   * </p>
51   *
52   * @see JavadocCommentsLexer
53   * @see JavadocCommentsParser
54   * @see JavadocNodeImpl
55   * @see JavaAstVisitor
56   * @noinspection JavadocReference
57   * @noinspectionreason JavadocReference - References are valid
58   */
59  public class JavadocCommentsAstVisitor extends JavadocCommentsParserBaseVisitor<JavadocNodeImpl> {
60  
61      /**
62       * All Javadoc tag token types.
63       */
64      private static final Set<Integer> JAVADOC_TAG_TYPES = Set.of(
65          JavadocCommentsLexer.CODE,
66          JavadocCommentsLexer.LINK,
67          JavadocCommentsLexer.LINKPLAIN,
68          JavadocCommentsLexer.VALUE,
69          JavadocCommentsLexer.INHERIT_DOC,
70          JavadocCommentsLexer.SUMMARY,
71          JavadocCommentsLexer.SYSTEM_PROPERTY,
72          JavadocCommentsLexer.INDEX,
73          JavadocCommentsLexer.RETURN,
74          JavadocCommentsLexer.LITERAL,
75          JavadocCommentsLexer.SNIPPET,
76          JavadocCommentsLexer.CUSTOM_NAME,
77          JavadocCommentsLexer.AUTHOR,
78          JavadocCommentsLexer.DEPRECATED,
79          JavadocCommentsLexer.PARAM,
80          JavadocCommentsLexer.THROWS,
81          JavadocCommentsLexer.EXCEPTION,
82          JavadocCommentsLexer.SINCE,
83          JavadocCommentsLexer.VERSION,
84          JavadocCommentsLexer.SEE,
85          JavadocCommentsLexer.LITERAL_HIDDEN,
86          JavadocCommentsLexer.USES,
87          JavadocCommentsLexer.PROVIDES,
88          JavadocCommentsLexer.SERIAL,
89          JavadocCommentsLexer.SERIAL_DATA,
90          JavadocCommentsLexer.SERIAL_FIELD
91      );
92  
93      /**
94       * Line number of the Block comment AST that is being parsed.
95       */
96      private final int blockCommentLineNumber;
97  
98      /**
99       * Javadoc Ident.
100      */
101     private final int javadocColumnNumber;
102 
103     /**
104      * Token stream to check for hidden tokens.
105      */
106     private final BufferedTokenStream tokens;
107 
108     /**
109      * A set of token indices used to track which tokens have already had their
110      * hidden tokens added to the AST.
111      */
112     private final Set<Integer> processedTokenIndices = new HashSet<>();
113 
114     /**
115      * Accumulator for consecutive TEXT tokens.
116      * This is used to merge multiple TEXT tokens into a single node.
117      */
118     private final TextAccumulator accumulator = new TextAccumulator();
119 
120     /**
121      * The first non-tight HTML tag encountered in the Javadoc comment, if any.
122      */
123     private DetailNode firstNonTightHtmlTag;
124 
125     /**
126      * Constructs a JavaAstVisitor with given token stream, line number, and column number.
127      *
128      * @param tokens the token stream to check for hidden tokens
129      * @param blockCommentLineNumber the line number of the block comment being parsed
130      * @param javadocColumnNumber the column number of the javadoc indent
131      */
132     public JavadocCommentsAstVisitor(CommonTokenStream tokens,
133                                      int blockCommentLineNumber, int javadocColumnNumber) {
134         this.tokens = tokens;
135         this.blockCommentLineNumber = blockCommentLineNumber;
136         this.javadocColumnNumber = javadocColumnNumber;
137     }
138 
139     @Override
140     public JavadocNodeImpl visitJavadoc(JavadocCommentsParser.JavadocContext ctx) {
141         return buildImaginaryNode(JavadocCommentsTokenTypes.JAVADOC_CONTENT, ctx);
142     }
143 
144     @Override
145     public JavadocNodeImpl visitMainDescription(JavadocCommentsParser.MainDescriptionContext ctx) {
146         return flattenedTree(ctx);
147     }
148 
149     @Override
150     public JavadocNodeImpl visitBlockTag(JavadocCommentsParser.BlockTagContext ctx) {
151         final JavadocNodeImpl blockTagNode =
152                 createImaginary(JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG);
153         final ParseTree tag = ctx.getChild(0);
154         final Token tagName = (Token) tag.getChild(1).getPayload();
155         final int tokenType = tagName.getType();
156         final JavadocNodeImpl specificTagNode = switch (tokenType) {
157             case JavadocCommentsLexer.AUTHOR ->
158                 buildImaginaryNode(JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG, ctx);
159             case JavadocCommentsLexer.DEPRECATED ->
160                 buildImaginaryNode(JavadocCommentsTokenTypes.DEPRECATED_BLOCK_TAG, ctx);
161             case JavadocCommentsLexer.RETURN ->
162                 buildImaginaryNode(JavadocCommentsTokenTypes.RETURN_BLOCK_TAG, ctx);
163             case JavadocCommentsLexer.PARAM ->
164                 buildImaginaryNode(JavadocCommentsTokenTypes.PARAM_BLOCK_TAG, ctx);
165             case JavadocCommentsLexer.THROWS ->
166                 buildImaginaryNode(JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, ctx);
167             case JavadocCommentsLexer.EXCEPTION ->
168                 buildImaginaryNode(JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG, ctx);
169             case JavadocCommentsLexer.SINCE ->
170                 buildImaginaryNode(JavadocCommentsTokenTypes.SINCE_BLOCK_TAG, ctx);
171             case JavadocCommentsLexer.VERSION ->
172                 buildImaginaryNode(JavadocCommentsTokenTypes.VERSION_BLOCK_TAG, ctx);
173             case JavadocCommentsLexer.SEE ->
174                 buildImaginaryNode(JavadocCommentsTokenTypes.SEE_BLOCK_TAG, ctx);
175             case JavadocCommentsLexer.LITERAL_HIDDEN ->
176                 buildImaginaryNode(JavadocCommentsTokenTypes.HIDDEN_BLOCK_TAG, ctx);
177             case JavadocCommentsLexer.USES ->
178                 buildImaginaryNode(JavadocCommentsTokenTypes.USES_BLOCK_TAG, ctx);
179             case JavadocCommentsLexer.PROVIDES ->
180                 buildImaginaryNode(JavadocCommentsTokenTypes.PROVIDES_BLOCK_TAG, ctx);
181             case JavadocCommentsLexer.SERIAL ->
182                 buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_BLOCK_TAG, ctx);
183             case JavadocCommentsLexer.SERIAL_DATA ->
184                 buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_DATA_BLOCK_TAG, ctx);
185             case JavadocCommentsLexer.SERIAL_FIELD ->
186                 buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_FIELD_BLOCK_TAG, ctx);
187             default ->
188                 buildImaginaryNode(JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG, ctx);
189         };
190         blockTagNode.addChild(specificTagNode);
191 
192         return blockTagNode;
193     }
194 
195     @Override
196     public JavadocNodeImpl visitAuthorTag(JavadocCommentsParser.AuthorTagContext ctx) {
197         return flattenedTree(ctx);
198     }
199 
200     @Override
201     public JavadocNodeImpl visitDeprecatedTag(JavadocCommentsParser.DeprecatedTagContext ctx) {
202         return flattenedTree(ctx);
203     }
204 
205     @Override
206     public JavadocNodeImpl visitReturnTag(JavadocCommentsParser.ReturnTagContext ctx) {
207         return flattenedTree(ctx);
208     }
209 
210     @Override
211     public JavadocNodeImpl visitParameterTag(JavadocCommentsParser.ParameterTagContext ctx) {
212         return flattenedTree(ctx);
213     }
214 
215     @Override
216     public JavadocNodeImpl visitThrowsTag(JavadocCommentsParser.ThrowsTagContext ctx) {
217         return flattenedTree(ctx);
218     }
219 
220     @Override
221     public JavadocNodeImpl visitExceptionTag(JavadocCommentsParser.ExceptionTagContext ctx) {
222         return flattenedTree(ctx);
223     }
224 
225     @Override
226     public JavadocNodeImpl visitSinceTag(JavadocCommentsParser.SinceTagContext ctx) {
227         return flattenedTree(ctx);
228     }
229 
230     @Override
231     public JavadocNodeImpl visitVersionTag(JavadocCommentsParser.VersionTagContext ctx) {
232         return flattenedTree(ctx);
233     }
234 
235     @Override
236     public JavadocNodeImpl visitSeeTag(JavadocCommentsParser.SeeTagContext ctx) {
237         return flattenedTree(ctx);
238     }
239 
240     @Override
241     public JavadocNodeImpl visitHiddenTag(JavadocCommentsParser.HiddenTagContext ctx) {
242         return flattenedTree(ctx);
243     }
244 
245     @Override
246     public JavadocNodeImpl visitUsesTag(JavadocCommentsParser.UsesTagContext ctx) {
247         return flattenedTree(ctx);
248     }
249 
250     @Override
251     public JavadocNodeImpl visitProvidesTag(JavadocCommentsParser.ProvidesTagContext ctx) {
252         return flattenedTree(ctx);
253     }
254 
255     @Override
256     public JavadocNodeImpl visitSerialTag(JavadocCommentsParser.SerialTagContext ctx) {
257         return flattenedTree(ctx);
258     }
259 
260     @Override
261     public JavadocNodeImpl visitSerialDataTag(JavadocCommentsParser.SerialDataTagContext ctx) {
262         return flattenedTree(ctx);
263     }
264 
265     @Override
266     public JavadocNodeImpl visitSerialFieldTag(JavadocCommentsParser.SerialFieldTagContext ctx) {
267         return flattenedTree(ctx);
268     }
269 
270     @Override
271     public JavadocNodeImpl visitCustomBlockTag(JavadocCommentsParser.CustomBlockTagContext ctx) {
272         return flattenedTree(ctx);
273     }
274 
275     @Override
276     public JavadocNodeImpl visitInlineTag(JavadocCommentsParser.InlineTagContext ctx) {
277         final JavadocNodeImpl inlineTagNode =
278                 createImaginary(JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG);
279         final ParseTree tagContent = ctx.inlineTagContent().getChild(0);
280         final Token tagName = (Token) tagContent.getChild(0).getPayload();
281         final int tokenType = tagName.getType();
282         final JavadocNodeImpl specificTagNode = switch (tokenType) {
283             case JavadocCommentsLexer.CODE ->
284                 buildImaginaryNode(JavadocCommentsTokenTypes.CODE_INLINE_TAG, ctx);
285             case JavadocCommentsLexer.LINK ->
286                 buildImaginaryNode(JavadocCommentsTokenTypes.LINK_INLINE_TAG, ctx);
287             case JavadocCommentsLexer.LINKPLAIN ->
288                 buildImaginaryNode(JavadocCommentsTokenTypes.LINKPLAIN_INLINE_TAG, ctx);
289             case JavadocCommentsLexer.VALUE ->
290                 buildImaginaryNode(JavadocCommentsTokenTypes.VALUE_INLINE_TAG, ctx);
291             case JavadocCommentsLexer.INHERIT_DOC ->
292                 buildImaginaryNode(JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG, ctx);
293             case JavadocCommentsLexer.SUMMARY ->
294                 buildImaginaryNode(JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG, ctx);
295             case JavadocCommentsLexer.SYSTEM_PROPERTY ->
296                 buildImaginaryNode(JavadocCommentsTokenTypes.SYSTEM_PROPERTY_INLINE_TAG, ctx);
297             case JavadocCommentsLexer.INDEX ->
298                 buildImaginaryNode(JavadocCommentsTokenTypes.INDEX_INLINE_TAG, ctx);
299             case JavadocCommentsLexer.RETURN ->
300                 buildImaginaryNode(JavadocCommentsTokenTypes.RETURN_INLINE_TAG, ctx);
301             case JavadocCommentsLexer.LITERAL ->
302                 buildImaginaryNode(JavadocCommentsTokenTypes.LITERAL_INLINE_TAG, ctx);
303             case JavadocCommentsLexer.SNIPPET ->
304                 buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_INLINE_TAG, ctx);
305             default -> buildImaginaryNode(JavadocCommentsTokenTypes.CUSTOM_INLINE_TAG, ctx);
306         };
307         inlineTagNode.addChild(specificTagNode);
308 
309         return inlineTagNode;
310     }
311 
312     @Override
313     public JavadocNodeImpl visitInlineTagContent(
314             JavadocCommentsParser.InlineTagContentContext ctx) {
315         return flattenedTree(ctx);
316     }
317 
318     @Override
319     public JavadocNodeImpl visitCodeInlineTag(JavadocCommentsParser.CodeInlineTagContext ctx) {
320         return flattenedTree(ctx);
321     }
322 
323     @Override
324     public JavadocNodeImpl visitLinkPlainInlineTag(
325             JavadocCommentsParser.LinkPlainInlineTagContext ctx) {
326         return flattenedTree(ctx);
327     }
328 
329     @Override
330     public JavadocNodeImpl visitLinkInlineTag(JavadocCommentsParser.LinkInlineTagContext ctx) {
331         return flattenedTree(ctx);
332     }
333 
334     @Override
335     public JavadocNodeImpl visitValueInlineTag(JavadocCommentsParser.ValueInlineTagContext ctx) {
336         return flattenedTree(ctx);
337     }
338 
339     @Override
340     public JavadocNodeImpl visitInheritDocInlineTag(
341             JavadocCommentsParser.InheritDocInlineTagContext ctx) {
342         return flattenedTree(ctx);
343     }
344 
345     @Override
346     public JavadocNodeImpl visitSummaryInlineTag(
347             JavadocCommentsParser.SummaryInlineTagContext ctx) {
348         return flattenedTree(ctx);
349     }
350 
351     @Override
352     public JavadocNodeImpl visitSystemPropertyInlineTag(
353             JavadocCommentsParser.SystemPropertyInlineTagContext ctx) {
354         return flattenedTree(ctx);
355     }
356 
357     @Override
358     public JavadocNodeImpl visitIndexInlineTag(JavadocCommentsParser.IndexInlineTagContext ctx) {
359         return flattenedTree(ctx);
360     }
361 
362     @Override
363     public JavadocNodeImpl visitReturnInlineTag(JavadocCommentsParser.ReturnInlineTagContext ctx) {
364         return flattenedTree(ctx);
365     }
366 
367     @Override
368     public JavadocNodeImpl visitLiteralInlineTag(
369             JavadocCommentsParser.LiteralInlineTagContext ctx) {
370         return flattenedTree(ctx);
371     }
372 
373     @Override
374     public JavadocNodeImpl visitSnippetInlineTag(
375             JavadocCommentsParser.SnippetInlineTagContext ctx) {
376         final JavadocNodeImpl dummyRoot = new JavadocNodeImpl();
377         if (!ctx.snippetAttributes.isEmpty()) {
378             final JavadocNodeImpl snippetAttributes =
379                     createImaginary(JavadocCommentsTokenTypes.SNIPPET_ATTRIBUTES);
380             ctx.snippetAttributes.forEach(snippetAttributeContext -> {
381                 final JavadocNodeImpl snippetAttribute = visit(snippetAttributeContext);
382                 snippetAttributes.addChild(snippetAttribute);
383             });
384             dummyRoot.addChild(snippetAttributes);
385         }
386         final TerminalNode colon = ctx.COLON();
387         if (colon != null) {
388             dummyRoot.addChild(create((Token) colon.getPayload()));
389         }
390         final JavadocCommentsParser.SnippetBodyContext snippetBody = ctx.snippetBody();
391         if (snippetBody != null) {
392             dummyRoot.addChild(visit(snippetBody));
393         }
394         return dummyRoot.getFirstChild();
395     }
396 
397     @Override
398     public JavadocNodeImpl visitCustomInlineTag(JavadocCommentsParser.CustomInlineTagContext ctx) {
399         return flattenedTree(ctx);
400     }
401 
402     @Override
403     public JavadocNodeImpl visitReference(JavadocCommentsParser.ReferenceContext ctx) {
404         return buildImaginaryNode(JavadocCommentsTokenTypes.REFERENCE, ctx);
405     }
406 
407     @Override
408     public JavadocNodeImpl visitTypeName(JavadocCommentsParser.TypeNameContext ctx) {
409         return flattenedTree(ctx);
410 
411     }
412 
413     @Override
414     public JavadocNodeImpl visitQualifiedName(JavadocCommentsParser.QualifiedNameContext ctx) {
415         return flattenedTree(ctx);
416     }
417 
418     @Override
419     public JavadocNodeImpl visitTypeArguments(JavadocCommentsParser.TypeArgumentsContext ctx) {
420         return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENTS, ctx);
421     }
422 
423     @Override
424     public JavadocNodeImpl visitTypeArgument(JavadocCommentsParser.TypeArgumentContext ctx) {
425         return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENT, ctx);
426     }
427 
428     @Override
429     public JavadocNodeImpl visitMemberReference(JavadocCommentsParser.MemberReferenceContext ctx) {
430         return buildImaginaryNode(JavadocCommentsTokenTypes.MEMBER_REFERENCE, ctx);
431     }
432 
433     @Override
434     public JavadocNodeImpl visitMethodReferenceWithoutHash(
435             JavadocCommentsParser.MethodReferenceWithoutHashContext ctx) {
436         return buildImaginaryNode(JavadocCommentsTokenTypes.MEMBER_REFERENCE, ctx);
437     }
438 
439     @Override
440     public JavadocNodeImpl visitParameterTypeList(
441             JavadocCommentsParser.ParameterTypeListContext ctx) {
442         return buildImaginaryNode(JavadocCommentsTokenTypes.PARAMETER_TYPE_LIST, ctx);
443     }
444 
445     @Override
446     public JavadocNodeImpl visitDescription(JavadocCommentsParser.DescriptionContext ctx) {
447         return buildImaginaryNode(JavadocCommentsTokenTypes.DESCRIPTION, ctx);
448     }
449 
450     @Override
451     public JavadocNodeImpl visitSnippetAttribute(
452             JavadocCommentsParser.SnippetAttributeContext ctx) {
453         return buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_ATTRIBUTE, ctx);
454     }
455 
456     @Override
457     public JavadocNodeImpl visitSnippetBody(JavadocCommentsParser.SnippetBodyContext ctx) {
458         return buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_BODY, ctx);
459     }
460 
461     @Override
462     public JavadocNodeImpl visitHtmlElement(JavadocCommentsParser.HtmlElementContext ctx) {
463         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_ELEMENT, ctx);
464     }
465 
466     @Override
467     public JavadocNodeImpl visitVoidElement(JavadocCommentsParser.VoidElementContext ctx) {
468         return buildImaginaryNode(JavadocCommentsTokenTypes.VOID_ELEMENT, ctx);
469     }
470 
471     @Override
472     public JavadocNodeImpl visitTightElement(JavadocCommentsParser.TightElementContext ctx) {
473         return flattenedTree(ctx);
474     }
475 
476     @Override
477     public JavadocNodeImpl visitNonTightElement(JavadocCommentsParser.NonTightElementContext ctx) {
478         if (firstNonTightHtmlTag == null) {
479             final ParseTree htmlTagStart = ctx.getChild(0);
480             final ParseTree tagNameToken = htmlTagStart.getChild(1);
481             firstNonTightHtmlTag = create((Token) tagNameToken.getPayload());
482         }
483         return flattenedTree(ctx);
484     }
485 
486     @Override
487     public JavadocNodeImpl visitSelfClosingElement(
488             JavadocCommentsParser.SelfClosingElementContext ctx) {
489         final JavadocNodeImpl javadocNode =
490                 createImaginary(JavadocCommentsTokenTypes.VOID_ELEMENT);
491         javadocNode.addChild(create((Token) ctx.TAG_OPEN().getPayload()));
492         javadocNode.addChild(create((Token) ctx.TAG_NAME().getPayload()));
493         if (!ctx.htmlAttributes.isEmpty()) {
494             final JavadocNodeImpl htmlAttributes =
495                     createImaginary(JavadocCommentsTokenTypes.HTML_ATTRIBUTES);
496             ctx.htmlAttributes.forEach(htmlAttributeContext -> {
497                 final JavadocNodeImpl htmlAttribute = visit(htmlAttributeContext);
498                 htmlAttributes.addChild(htmlAttribute);
499             });
500             javadocNode.addChild(htmlAttributes);
501         }
502 
503         javadocNode.addChild(create((Token) ctx.TAG_SLASH_CLOSE().getPayload()));
504         return javadocNode;
505     }
506 
507     @Override
508     public JavadocNodeImpl visitHtmlTagStart(JavadocCommentsParser.HtmlTagStartContext ctx) {
509         final JavadocNodeImpl javadocNode =
510                 createImaginary(JavadocCommentsTokenTypes.HTML_TAG_START);
511         javadocNode.addChild(create((Token) ctx.TAG_OPEN().getPayload()));
512         javadocNode.addChild(create((Token) ctx.TAG_NAME().getPayload()));
513         if (!ctx.htmlAttributes.isEmpty()) {
514             final JavadocNodeImpl htmlAttributes =
515                     createImaginary(JavadocCommentsTokenTypes.HTML_ATTRIBUTES);
516             ctx.htmlAttributes.forEach(htmlAttributeContext -> {
517                 final JavadocNodeImpl htmlAttribute = visit(htmlAttributeContext);
518                 htmlAttributes.addChild(htmlAttribute);
519             });
520             javadocNode.addChild(htmlAttributes);
521         }
522 
523         final Token tagClose = (Token) ctx.TAG_CLOSE().getPayload();
524         addHiddenTokensToTheLeft(tagClose, javadocNode);
525         javadocNode.addChild(create(tagClose));
526         return javadocNode;
527     }
528 
529     @Override
530     public JavadocNodeImpl visitHtmlTagEnd(JavadocCommentsParser.HtmlTagEndContext ctx) {
531         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_TAG_END, ctx);
532     }
533 
534     @Override
535     public JavadocNodeImpl visitHtmlAttribute(JavadocCommentsParser.HtmlAttributeContext ctx) {
536         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_ATTRIBUTE, ctx);
537     }
538 
539     @Override
540     public JavadocNodeImpl visitHtmlContent(JavadocCommentsParser.HtmlContentContext ctx) {
541         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_CONTENT, ctx);
542     }
543 
544     @Override
545     public JavadocNodeImpl visitNonTightHtmlContent(
546             JavadocCommentsParser.NonTightHtmlContentContext ctx) {
547         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_CONTENT, ctx);
548     }
549 
550     @Override
551     public JavadocNodeImpl visitHtmlComment(JavadocCommentsParser.HtmlCommentContext ctx) {
552         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_COMMENT, ctx);
553     }
554 
555     @Override
556     public JavadocNodeImpl visitHtmlCommentContent(
557             JavadocCommentsParser.HtmlCommentContentContext ctx) {
558         return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_COMMENT_CONTENT, ctx);
559     }
560 
561     /**
562      * Creates an imaginary JavadocNodeImpl of the given token type and
563      * processes all children of the given ParserRuleContext.
564      *
565      * @param tokenType the token type of this JavadocNodeImpl
566      * @param ctx the ParserRuleContext whose children are to be processed
567      * @return new JavadocNodeImpl of given type with processed children
568      */
569     private JavadocNodeImpl buildImaginaryNode(int tokenType, ParserRuleContext ctx) {
570         final JavadocNodeImpl javadocNode = createImaginary(tokenType);
571         processChildren(javadocNode, ctx.children);
572         return javadocNode;
573     }
574 
575     /**
576      * Builds the AST for a particular node, then returns a "flattened" tree
577      * of siblings.
578      *
579      * @param ctx the ParserRuleContext to base tree on
580      * @return flattened DetailAstImpl
581      */
582     private JavadocNodeImpl flattenedTree(ParserRuleContext ctx) {
583         final JavadocNodeImpl dummyNode = new JavadocNodeImpl();
584         processChildren(dummyNode, ctx.children);
585         return dummyNode.getFirstChild();
586     }
587 
588     /**
589      * Adds all the children from the given ParseTree or ParserRuleContext
590      * list to the parent JavadocNodeImpl.
591      *
592      * @param parent   the JavadocNodeImpl to add children to
593      * @param children the list of children to add
594      */
595     private void processChildren(JavadocNodeImpl parent, List<? extends ParseTree> children) {
596         for (ParseTree child : children) {
597             if (child instanceof TerminalNode terminalNode) {
598                 final Token token = (Token) terminalNode.getPayload();
599 
600                 // Add hidden tokens before this token
601                 addHiddenTokensToTheLeft(token, parent);
602 
603                 if (isTextToken(token)) {
604                     accumulator.append(token);
605                 }
606                 else if (token.getType() != Token.EOF) {
607                     parent.addChild(create(token));
608                 }
609             }
610             else {
611                 accumulator.flushTo(parent);
612                 final Token token = ((ParserRuleContext) child).getStart();
613                 addHiddenTokensToTheLeft(token, parent);
614                 parent.addChild(visit(child));
615             }
616         }
617 
618         accumulator.flushTo(parent);
619     }
620 
621     /**
622      * Checks whether a token is a Javadoc text token.
623      *
624      * @param token the token to check
625      * @return true if the token is a text token, false otherwise
626      */
627     private static boolean isTextToken(Token token) {
628         return token.getType() == JavadocCommentsTokenTypes.TEXT;
629     }
630 
631     /**
632      * Checks whether a token is a formatting token with multiple leading asterisks.
633      *
634      * @param token the token to check
635      * @return true if the token contains multiple leading asterisks
636      */
637     private static boolean isMultipleLeadingAsterisks(Token token) {
638         boolean result = false;
639 
640         if (isLeadingAsterisk(token)) {
641             final String leadingAsterisks = getLeadingAsterisksText(token);
642             result = leadingAsterisks.length() > 1;
643         }
644 
645         return result;
646     }
647 
648     /**
649      * Checks whether a token is a leading asterisk formatting token.
650      *
651      * @param token the token to check
652      * @return true if the token is a leading asterisk token
653      */
654     private static boolean isLeadingAsterisk(Token token) {
655         return token.getType() == JavadocCommentsLexer.LEADING_ASTERISK;
656     }
657 
658     /**
659      * Returns only leading asterisks from the token text, without indentation.
660      *
661      * @param token the token to process
662      * @return token text starting at the first asterisk
663      */
664     private static String getLeadingAsterisksText(Token token) {
665         return token.getText().substring(token.getText().indexOf('*'));
666     }
667 
668     /**
669      * Adds hidden tokens to the left of the given token to the parent node.
670      * Ensures text accumulation is flushed before adding hidden tokens.
671      * Hidden tokens are only added once per unique token index.
672      *
673      * @param token      the token whose hidden tokens should be added
674      * @param parent     the parent node to which hidden tokens are added
675      */
676     private void addHiddenTokensToTheLeft(Token token, JavadocNodeImpl parent) {
677         final boolean alreadyProcessed = !processedTokenIndices.add(token.getTokenIndex());
678 
679         if (!alreadyProcessed) {
680             final int tokenIndex = token.getTokenIndex();
681             final List<Token> hiddenTokens = tokens.getHiddenTokensToLeft(tokenIndex);
682             if (hiddenTokens != null) {
683                 accumulator.flushTo(parent);
684                 for (Token hiddenToken : hiddenTokens) {
685                     parent.addChild(create(hiddenToken));
686                 }
687             }
688         }
689     }
690 
691     /**
692      * Creates a JavadocNodeImpl from the given token.
693      *
694      * @param token the token to create the JavadocNodeImpl from
695      * @return a new JavadocNodeImpl initialized with the token
696      */
697     private JavadocNodeImpl create(Token token) {
698         final JavadocNodeImpl node = new JavadocNodeImpl();
699         node.initialize(token);
700 
701         // adjust line number to the position of the block comment
702         node.setLineNumber(node.getLineNumber() + blockCommentLineNumber);
703 
704         // adjust first line to indent of /**
705         if (node.getLineNumber() == blockCommentLineNumber) {
706             node.setColumnNumber(node.getColumnNumber() + javadocColumnNumber);
707         }
708 
709         final int tokenType = token.getType();
710         if (isLeadingAsterisk(token)) {
711             final String leadingAsterisks = getLeadingAsterisksText(token);
712             node.setColumnNumber(node.getColumnNumber() + token.getText().indexOf('*'));
713             node.setText(leadingAsterisks);
714         }
715         if (isJavadocTag(tokenType)) {
716             node.setType(JavadocCommentsTokenTypes.TAG_NAME);
717         }
718         if (tokenType == JavadocCommentsLexer.WS) {
719             node.setType(JavadocCommentsTokenTypes.TEXT);
720         }
721         if (isMultipleLeadingAsterisks(token)) {
722             node.setType(JavadocCommentsTokenTypes.LEADING_ASTERISKS);
723         }
724 
725         return node;
726     }
727 
728     /**
729      * Checks if the given token type is a Javadoc tag.
730      *
731      * @param type the token type to check
732      * @return true if the token type is a Javadoc tag, false otherwise
733      */
734     private static boolean isJavadocTag(int type) {
735         return JAVADOC_TAG_TYPES.contains(type);
736     }
737 
738     /**
739      * Create a JavadocNodeImpl from a given token and token type. This method should be used for
740      * imaginary nodes only, i.e. {@literal 'JAVADOC_INLINE_TAG -> JAVADOC_INLINE_TAG'},
741      * where the text on the RHS matches the text on the LHS.
742      *
743      * @param tokenType the token type of this JavadocNodeImpl
744      * @return new JavadocNodeImpl of given type
745      */
746     private JavadocNodeImpl createImaginary(int tokenType) {
747         final JavadocNodeImpl node = new JavadocNodeImpl();
748         node.setType(tokenType);
749         node.setText(JavadocUtil.getTokenName(tokenType));
750         node.setLineNumber(blockCommentLineNumber);
751         node.setColumnNumber(javadocColumnNumber);
752         return node;
753     }
754 
755     /**
756      * Returns the first non-tight HTML tag encountered in the Javadoc comment, if any.
757      *
758      * @return the first non-tight HTML tag, or null if none was found
759      */
760     public DetailNode getFirstNonTightHtmlTag() {
761         return firstNonTightHtmlTag;
762     }
763 
764     /**
765      * A small utility to accumulate consecutive TEXT tokens into one node,
766      * preserving the starting token for accurate location metadata.
767      */
768     private final class TextAccumulator {
769         /**
770          * Buffer to accumulate TEXT token texts.
771          *
772          * @noinspection StringBufferField
773          * @noinspectionreason StringBufferField - We want to reuse the same buffer to avoid
774          */
775         private final StringBuilder buffer = new StringBuilder(256);
776 
777         /**
778          * The first token in the accumulation, used for line/column info.
779          */
780         private Token startToken;
781 
782         /**
783          * Creates a new {@code TextAccumulator} instance.
784          */
785         private TextAccumulator() {
786             // no code by default
787         }
788 
789         /**
790          * Appends a TEXT token's text to the buffer and tracks the first token.
791          *
792          * @param token the token to accumulate
793          */
794         /* package */ void append(Token token) {
795             if (buffer.isEmpty()) {
796                 startToken = token;
797             }
798             buffer.append(token.getText());
799         }
800 
801         /**
802          * Flushes the accumulated buffer into a single {@link JavadocNodeImpl} node
803          * and adds it to the given parent. Clears the buffer after flushing.
804          *
805          * @param parent the parent node to add the new node to
806          */
807         /* package */ void flushTo(JavadocNodeImpl parent) {
808             if (!buffer.isEmpty()) {
809                 final JavadocNodeImpl startNode = create(startToken);
810                 startNode.setText(buffer.toString());
811                 parent.addChild(startNode);
812                 buffer.setLength(0);
813             }
814         }
815     }
816 
817 }