001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 * @noinspection JavadocReference
057 * @noinspectionreason JavadocReference - References are valid
058 */
059public class JavadocCommentsAstVisitor extends JavadocCommentsParserBaseVisitor<JavadocNodeImpl> {
060
061    /**
062     * All Javadoc tag token types.
063     */
064    private static final Set<Integer> JAVADOC_TAG_TYPES = Set.of(
065        JavadocCommentsLexer.CODE,
066        JavadocCommentsLexer.LINK,
067        JavadocCommentsLexer.LINKPLAIN,
068        JavadocCommentsLexer.VALUE,
069        JavadocCommentsLexer.INHERIT_DOC,
070        JavadocCommentsLexer.SUMMARY,
071        JavadocCommentsLexer.SYSTEM_PROPERTY,
072        JavadocCommentsLexer.INDEX,
073        JavadocCommentsLexer.RETURN,
074        JavadocCommentsLexer.LITERAL,
075        JavadocCommentsLexer.SNIPPET,
076        JavadocCommentsLexer.CUSTOM_NAME,
077        JavadocCommentsLexer.AUTHOR,
078        JavadocCommentsLexer.DEPRECATED,
079        JavadocCommentsLexer.PARAM,
080        JavadocCommentsLexer.THROWS,
081        JavadocCommentsLexer.EXCEPTION,
082        JavadocCommentsLexer.SINCE,
083        JavadocCommentsLexer.VERSION,
084        JavadocCommentsLexer.SEE,
085        JavadocCommentsLexer.LITERAL_HIDDEN,
086        JavadocCommentsLexer.USES,
087        JavadocCommentsLexer.PROVIDES,
088        JavadocCommentsLexer.SERIAL,
089        JavadocCommentsLexer.SERIAL_DATA,
090        JavadocCommentsLexer.SERIAL_FIELD
091    );
092
093    /**
094     * Line number of the Block comment AST that is being parsed.
095     */
096    private final int blockCommentLineNumber;
097
098    /**
099     * 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     * Adds hidden tokens to the left of the given token to the parent node.
633     * Ensures text accumulation is flushed before adding hidden tokens.
634     * Hidden tokens are only added once per unique token index.
635     *
636     * @param token      the token whose hidden tokens should be added
637     * @param parent     the parent node to which hidden tokens are added
638     */
639    private void addHiddenTokensToTheLeft(Token token, JavadocNodeImpl parent) {
640        final boolean alreadyProcessed = !processedTokenIndices.add(token.getTokenIndex());
641
642        if (!alreadyProcessed) {
643            final int tokenIndex = token.getTokenIndex();
644            final List<Token> hiddenTokens = tokens.getHiddenTokensToLeft(tokenIndex);
645            if (hiddenTokens != null) {
646                accumulator.flushTo(parent);
647                for (Token hiddenToken : hiddenTokens) {
648                    parent.addChild(create(hiddenToken));
649                }
650            }
651        }
652    }
653
654    /**
655     * Creates a JavadocNodeImpl from the given token.
656     *
657     * @param token the token to create the JavadocNodeImpl from
658     * @return a new JavadocNodeImpl initialized with the token
659     */
660    private JavadocNodeImpl create(Token token) {
661        final JavadocNodeImpl node = new JavadocNodeImpl();
662        node.initialize(token);
663
664        // adjust line number to the position of the block comment
665        node.setLineNumber(node.getLineNumber() + blockCommentLineNumber);
666
667        // adjust first line to indent of /**
668        if (node.getLineNumber() == blockCommentLineNumber) {
669            node.setColumnNumber(node.getColumnNumber() + javadocColumnNumber);
670        }
671
672        final int tokenType = token.getType();
673        if (isJavadocTag(tokenType)) {
674            node.setType(JavadocCommentsTokenTypes.TAG_NAME);
675        }
676        if (tokenType == JavadocCommentsLexer.WS) {
677            node.setType(JavadocCommentsTokenTypes.TEXT);
678        }
679
680        return node;
681    }
682
683    /**
684     * Checks if the given token type is a Javadoc tag.
685     *
686     * @param type the token type to check
687     * @return true if the token type is a Javadoc tag, false otherwise
688     */
689    private static boolean isJavadocTag(int type) {
690        return JAVADOC_TAG_TYPES.contains(type);
691    }
692
693    /**
694     * Create a JavadocNodeImpl from a given token and token type. This method should be used for
695     * imaginary nodes only, i.e. {@literal 'JAVADOC_INLINE_TAG -> JAVADOC_INLINE_TAG'},
696     * where the text on the RHS matches the text on the LHS.
697     *
698     * @param tokenType the token type of this JavadocNodeImpl
699     * @return new JavadocNodeImpl of given type
700     */
701    private JavadocNodeImpl createImaginary(int tokenType) {
702        final JavadocNodeImpl node = new JavadocNodeImpl();
703        node.setType(tokenType);
704        node.setText(JavadocUtil.getTokenName(tokenType));
705        node.setLineNumber(blockCommentLineNumber);
706        node.setColumnNumber(javadocColumnNumber);
707        return node;
708    }
709
710    /**
711     * Returns the first non-tight HTML tag encountered in the Javadoc comment, if any.
712     *
713     * @return the first non-tight HTML tag, or null if none was found
714     */
715    public DetailNode getFirstNonTightHtmlTag() {
716        return firstNonTightHtmlTag;
717    }
718
719    /**
720     * A small utility to accumulate consecutive TEXT tokens into one node,
721     * preserving the starting token for accurate location metadata.
722     */
723    private final class TextAccumulator {
724        /**
725         * Buffer to accumulate TEXT token texts.
726         *
727         * @noinspection StringBufferField
728         * @noinspectionreason StringBufferField - We want to reuse the same buffer to avoid
729         */
730        private final StringBuilder buffer = new StringBuilder(256);
731
732        /**
733         * The first token in the accumulation, used for line/column info.
734         */
735        private Token startToken;
736
737        /**
738         * Creates a new {@code TextAccumulator} instance.
739         */
740        private TextAccumulator() {
741            // no code by default
742        }
743
744        /**
745         * Appends a TEXT token's text to the buffer and tracks the first token.
746         *
747         * @param token the token to accumulate
748         */
749        /* package */ void append(Token token) {
750            if (buffer.isEmpty()) {
751                startToken = token;
752            }
753            buffer.append(token.getText());
754        }
755
756        /**
757         * Flushes the accumulated buffer into a single {@link JavadocNodeImpl} node
758         * and adds it to the given parent. Clears the buffer after flushing.
759         *
760         * @param parent the parent node to add the new node to
761         */
762        /* package */ void flushTo(JavadocNodeImpl parent) {
763            if (!buffer.isEmpty()) {
764                final JavadocNodeImpl startNode = create(startToken);
765                startNode.setText(buffer.toString());
766                parent.addChild(startNode);
767                buffer.setLength(0);
768            }
769        }
770    }
771
772}