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