001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.util.HashSet;
023import java.util.List;
024import java.util.Set;
025
026import org.antlr.v4.runtime.BufferedTokenStream;
027import org.antlr.v4.runtime.CommonTokenStream;
028import org.antlr.v4.runtime.ParserRuleContext;
029import org.antlr.v4.runtime.Token;
030import org.antlr.v4.runtime.tree.ParseTree;
031import org.antlr.v4.runtime.tree.TerminalNode;
032
033import com.puppycrawl.tools.checkstyle.api.DetailNode;
034import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
036import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsLexer;
037import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParser;
038import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParserBaseVisitor;
039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
040
041/**
042 * Visitor class used to build Checkstyle's Javadoc AST from the parse tree
043 * produced by {@link JavadocCommentsParser}. Each overridden {@code visit...}
044 * method visits children of a parse tree node (subrules) or creates terminal
045 * nodes (tokens), and returns a {@link JavadocNodeImpl} subtree as the result.
046 *
047 * <p>
048 * The order of {@code visit...} methods in {@code JavaAstVisitor.java} and production rules in
049 * {@code JavaLanguageParser.g4} should be consistent to ease maintenance.
050 * </p>
051 *
052 * @see JavadocCommentsLexer
053 * @see JavadocCommentsParser
054 * @see JavadocNodeImpl
055 * @see JavaAstVisitor
056 */
057public class JavadocCommentsAstVisitor extends JavadocCommentsParserBaseVisitor<JavadocNodeImpl> {
058
059    /**
060     * All Javadoc tag token types.
061     */
062    private static final Set<Integer> JAVADOC_TAG_TYPES = Set.of(
063        JavadocCommentsLexer.CODE,
064        JavadocCommentsLexer.LINK,
065        JavadocCommentsLexer.LINKPLAIN,
066        JavadocCommentsLexer.VALUE,
067        JavadocCommentsLexer.INHERIT_DOC,
068        JavadocCommentsLexer.SUMMARY,
069        JavadocCommentsLexer.SYSTEM_PROPERTY,
070        JavadocCommentsLexer.INDEX,
071        JavadocCommentsLexer.RETURN,
072        JavadocCommentsLexer.LITERAL,
073        JavadocCommentsLexer.SNIPPET,
074        JavadocCommentsLexer.CUSTOM_NAME,
075        JavadocCommentsLexer.AUTHOR,
076        JavadocCommentsLexer.DEPRECATED,
077        JavadocCommentsLexer.PARAM,
078        JavadocCommentsLexer.THROWS,
079        JavadocCommentsLexer.EXCEPTION,
080        JavadocCommentsLexer.SINCE,
081        JavadocCommentsLexer.VERSION,
082        JavadocCommentsLexer.SEE,
083        JavadocCommentsLexer.LITERAL_HIDDEN,
084        JavadocCommentsLexer.USES,
085        JavadocCommentsLexer.PROVIDES,
086        JavadocCommentsLexer.SERIAL,
087        JavadocCommentsLexer.SERIAL_DATA,
088        JavadocCommentsLexer.SERIAL_FIELD
089    );
090
091    /**
092     * Line number of the Block comment AST that is being parsed.
093     */
094    private final int blockCommentLineNumber;
095
096    /**
097     * Javadoc Ident.
098     */
099    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}