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        if (ctx.COLON() != null) {
387            dummyRoot.addChild(create((Token) ctx.COLON().getPayload()));
388        }
389        if (ctx.snippetBody() != null) {
390            dummyRoot.addChild(visit(ctx.snippetBody()));
391        }
392        return dummyRoot.getFirstChild();
393    }
394
395    @Override
396    public JavadocNodeImpl visitCustomInlineTag(JavadocCommentsParser.CustomInlineTagContext ctx) {
397        return flattenedTree(ctx);
398    }
399
400    @Override
401    public JavadocNodeImpl visitReference(JavadocCommentsParser.ReferenceContext ctx) {
402        return buildImaginaryNode(JavadocCommentsTokenTypes.REFERENCE, ctx);
403    }
404
405    @Override
406    public JavadocNodeImpl visitTypeName(JavadocCommentsParser.TypeNameContext ctx) {
407        return flattenedTree(ctx);
408
409    }
410
411    @Override
412    public JavadocNodeImpl visitQualifiedName(JavadocCommentsParser.QualifiedNameContext ctx) {
413        return flattenedTree(ctx);
414    }
415
416    @Override
417    public JavadocNodeImpl visitTypeArguments(JavadocCommentsParser.TypeArgumentsContext ctx) {
418        return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENTS, ctx);
419    }
420
421    @Override
422    public JavadocNodeImpl visitTypeArgument(JavadocCommentsParser.TypeArgumentContext ctx) {
423        return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENT, ctx);
424    }
425
426    @Override
427    public JavadocNodeImpl visitMemberReference(JavadocCommentsParser.MemberReferenceContext ctx) {
428        return buildImaginaryNode(JavadocCommentsTokenTypes.MEMBER_REFERENCE, ctx);
429    }
430
431    @Override
432    public JavadocNodeImpl visitParameterTypeList(
433            JavadocCommentsParser.ParameterTypeListContext ctx) {
434        return buildImaginaryNode(JavadocCommentsTokenTypes.PARAMETER_TYPE_LIST, ctx);
435    }
436
437    @Override
438    public JavadocNodeImpl visitDescription(JavadocCommentsParser.DescriptionContext ctx) {
439        return buildImaginaryNode(JavadocCommentsTokenTypes.DESCRIPTION, ctx);
440    }
441
442    @Override
443    public JavadocNodeImpl visitSnippetAttribute(
444            JavadocCommentsParser.SnippetAttributeContext ctx) {
445        return buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_ATTRIBUTE, ctx);
446    }
447
448    @Override
449    public JavadocNodeImpl visitSnippetBody(JavadocCommentsParser.SnippetBodyContext ctx) {
450        return buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_BODY, ctx);
451    }
452
453    @Override
454    public JavadocNodeImpl visitHtmlElement(JavadocCommentsParser.HtmlElementContext ctx) {
455        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_ELEMENT, ctx);
456    }
457
458    @Override
459    public JavadocNodeImpl visitVoidElement(JavadocCommentsParser.VoidElementContext ctx) {
460        return buildImaginaryNode(JavadocCommentsTokenTypes.VOID_ELEMENT, ctx);
461    }
462
463    @Override
464    public JavadocNodeImpl visitTightElement(JavadocCommentsParser.TightElementContext ctx) {
465        return flattenedTree(ctx);
466    }
467
468    @Override
469    public JavadocNodeImpl visitNonTightElement(JavadocCommentsParser.NonTightElementContext ctx) {
470        if (firstNonTightHtmlTag == null) {
471            final ParseTree htmlTagStart = ctx.getChild(0);
472            final ParseTree tagNameToken = htmlTagStart.getChild(1);
473            firstNonTightHtmlTag = create((Token) tagNameToken.getPayload());
474        }
475        return flattenedTree(ctx);
476    }
477
478    @Override
479    public JavadocNodeImpl visitSelfClosingElement(
480            JavadocCommentsParser.SelfClosingElementContext ctx) {
481        final JavadocNodeImpl javadocNode =
482                createImaginary(JavadocCommentsTokenTypes.VOID_ELEMENT);
483        javadocNode.addChild(create((Token) ctx.TAG_OPEN().getPayload()));
484        javadocNode.addChild(create((Token) ctx.TAG_NAME().getPayload()));
485        if (!ctx.htmlAttributes.isEmpty()) {
486            final JavadocNodeImpl htmlAttributes =
487                    createImaginary(JavadocCommentsTokenTypes.HTML_ATTRIBUTES);
488            ctx.htmlAttributes.forEach(htmlAttributeContext -> {
489                final JavadocNodeImpl htmlAttribute = visit(htmlAttributeContext);
490                htmlAttributes.addChild(htmlAttribute);
491            });
492            javadocNode.addChild(htmlAttributes);
493        }
494
495        javadocNode.addChild(create((Token) ctx.TAG_SLASH_CLOSE().getPayload()));
496        return javadocNode;
497    }
498
499    @Override
500    public JavadocNodeImpl visitHtmlTagStart(JavadocCommentsParser.HtmlTagStartContext ctx) {
501        final JavadocNodeImpl javadocNode =
502                createImaginary(JavadocCommentsTokenTypes.HTML_TAG_START);
503        javadocNode.addChild(create((Token) ctx.TAG_OPEN().getPayload()));
504        javadocNode.addChild(create((Token) ctx.TAG_NAME().getPayload()));
505        if (!ctx.htmlAttributes.isEmpty()) {
506            final JavadocNodeImpl htmlAttributes =
507                    createImaginary(JavadocCommentsTokenTypes.HTML_ATTRIBUTES);
508            ctx.htmlAttributes.forEach(htmlAttributeContext -> {
509                final JavadocNodeImpl htmlAttribute = visit(htmlAttributeContext);
510                htmlAttributes.addChild(htmlAttribute);
511            });
512            javadocNode.addChild(htmlAttributes);
513        }
514
515        final Token tagClose = (Token) ctx.TAG_CLOSE().getPayload();
516        addHiddenTokensToTheLeft(tagClose, javadocNode);
517        javadocNode.addChild(create(tagClose));
518        return javadocNode;
519    }
520
521    @Override
522    public JavadocNodeImpl visitHtmlTagEnd(JavadocCommentsParser.HtmlTagEndContext ctx) {
523        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_TAG_END, ctx);
524    }
525
526    @Override
527    public JavadocNodeImpl visitHtmlAttribute(JavadocCommentsParser.HtmlAttributeContext ctx) {
528        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_ATTRIBUTE, ctx);
529    }
530
531    @Override
532    public JavadocNodeImpl visitHtmlContent(JavadocCommentsParser.HtmlContentContext ctx) {
533        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_CONTENT, ctx);
534    }
535
536    @Override
537    public JavadocNodeImpl visitNonTightHtmlContent(
538            JavadocCommentsParser.NonTightHtmlContentContext ctx) {
539        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_CONTENT, ctx);
540    }
541
542    @Override
543    public JavadocNodeImpl visitHtmlComment(JavadocCommentsParser.HtmlCommentContext ctx) {
544        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_COMMENT, ctx);
545    }
546
547    @Override
548    public JavadocNodeImpl visitHtmlCommentContent(
549            JavadocCommentsParser.HtmlCommentContentContext ctx) {
550        return buildImaginaryNode(JavadocCommentsTokenTypes.HTML_COMMENT_CONTENT, ctx);
551    }
552
553    /**
554     * Creates an imaginary JavadocNodeImpl of the given token type and
555     * processes all children of the given ParserRuleContext.
556     *
557     * @param tokenType the token type of this JavadocNodeImpl
558     * @param ctx the ParserRuleContext whose children are to be processed
559     * @return new JavadocNodeImpl of given type with processed children
560     */
561    private JavadocNodeImpl buildImaginaryNode(int tokenType, ParserRuleContext ctx) {
562        final JavadocNodeImpl javadocNode = createImaginary(tokenType);
563        processChildren(javadocNode, ctx.children);
564        return javadocNode;
565    }
566
567    /**
568     * Builds the AST for a particular node, then returns a "flattened" tree
569     * of siblings.
570     *
571     * @param ctx the ParserRuleContext to base tree on
572     * @return flattened DetailAstImpl
573     */
574    private JavadocNodeImpl flattenedTree(ParserRuleContext ctx) {
575        final JavadocNodeImpl dummyNode = new JavadocNodeImpl();
576        processChildren(dummyNode, ctx.children);
577        return dummyNode.getFirstChild();
578    }
579
580    /**
581     * Adds all the children from the given ParseTree or ParserRuleContext
582     * list to the parent JavadocNodeImpl.
583     *
584     * @param parent   the JavadocNodeImpl to add children to
585     * @param children the list of children to add
586     */
587    private void processChildren(JavadocNodeImpl parent, List<? extends ParseTree> children) {
588        for (ParseTree child : children) {
589            if (child instanceof TerminalNode terminalNode) {
590                final Token token = (Token) terminalNode.getPayload();
591
592                // Add hidden tokens before this token
593                addHiddenTokensToTheLeft(token, parent);
594
595                if (isTextToken(token)) {
596                    accumulator.append(token);
597                }
598                else if (token.getType() != Token.EOF) {
599                    parent.addChild(create(token));
600                }
601            }
602            else {
603                accumulator.flushTo(parent);
604                final Token token = ((ParserRuleContext) child).getStart();
605                addHiddenTokensToTheLeft(token, parent);
606                parent.addChild(visit(child));
607            }
608        }
609
610        accumulator.flushTo(parent);
611    }
612
613    /**
614     * Checks whether a token is a Javadoc text token.
615     *
616     * @param token the token to check
617     * @return true if the token is a text token, false otherwise
618     */
619    private static boolean isTextToken(Token token) {
620        return token.getType() == JavadocCommentsTokenTypes.TEXT;
621    }
622
623    /**
624     * Adds hidden tokens to the left of the given token to the parent node.
625     * Ensures text accumulation is flushed before adding hidden tokens.
626     * Hidden tokens are only added once per unique token index.
627     *
628     * @param token      the token whose hidden tokens should be added
629     * @param parent     the parent node to which hidden tokens are added
630     */
631    private void addHiddenTokensToTheLeft(Token token, JavadocNodeImpl parent) {
632        final boolean alreadyProcessed = !processedTokenIndices.add(token.getTokenIndex());
633
634        if (!alreadyProcessed) {
635            final int tokenIndex = token.getTokenIndex();
636            final List<Token> hiddenTokens = tokens.getHiddenTokensToLeft(tokenIndex);
637            if (hiddenTokens != null) {
638                accumulator.flushTo(parent);
639                for (Token hiddenToken : hiddenTokens) {
640                    parent.addChild(create(hiddenToken));
641                }
642            }
643        }
644    }
645
646    /**
647     * Creates a JavadocNodeImpl from the given token.
648     *
649     * @param token the token to create the JavadocNodeImpl from
650     * @return a new JavadocNodeImpl initialized with the token
651     */
652    private JavadocNodeImpl create(Token token) {
653        final JavadocNodeImpl node = new JavadocNodeImpl();
654        node.initialize(token);
655
656        // adjust line number to the position of the block comment
657        node.setLineNumber(node.getLineNumber() + blockCommentLineNumber);
658
659        // adjust first line to indent of /**
660        if (node.getLineNumber() == blockCommentLineNumber) {
661            node.setColumnNumber(node.getColumnNumber() + javadocColumnNumber);
662        }
663
664        if (isJavadocTag(token.getType())) {
665            node.setType(JavadocCommentsTokenTypes.TAG_NAME);
666        }
667
668        if (token.getType() == JavadocCommentsLexer.WS) {
669            node.setType(JavadocCommentsTokenTypes.TEXT);
670        }
671
672        return node;
673    }
674
675    /**
676     * Checks if the given token type is a Javadoc tag.
677     *
678     * @param type the token type to check
679     * @return true if the token type is a Javadoc tag, false otherwise
680     */
681    private static boolean isJavadocTag(int type) {
682        return JAVADOC_TAG_TYPES.contains(type);
683    }
684
685    /**
686     * Create a JavadocNodeImpl from a given token and token type. This method
687     * should be used for imaginary nodes only, i.e. 'JAVADOC_INLINE_TAG -&gt; JAVADOC_INLINE_TAG',
688     * where the text on the RHS matches the text on the LHS.
689     *
690     * @param tokenType the token type of this JavadocNodeImpl
691     * @return new JavadocNodeImpl of given type
692     */
693    private JavadocNodeImpl createImaginary(int tokenType) {
694        final JavadocNodeImpl node = new JavadocNodeImpl();
695        node.setType(tokenType);
696        node.setText(JavadocUtil.getTokenName(tokenType));
697        node.setLineNumber(blockCommentLineNumber);
698        node.setColumnNumber(javadocColumnNumber);
699        return node;
700    }
701
702    /**
703     * Returns the first non-tight HTML tag encountered in the Javadoc comment, if any.
704     *
705     * @return the first non-tight HTML tag, or null if none was found
706     */
707    public DetailNode getFirstNonTightHtmlTag() {
708        return firstNonTightHtmlTag;
709    }
710
711    /**
712     * A small utility to accumulate consecutive TEXT tokens into one node,
713     * preserving the starting token for accurate location metadata.
714     */
715    private final class TextAccumulator {
716        /**
717         * Buffer to accumulate TEXT token texts.
718         *
719         * @noinspection StringBufferField
720         * @noinspectionreason StringBufferField - We want to reuse the same buffer to avoid
721         */
722        private final StringBuilder buffer = new StringBuilder(256);
723
724        /**
725         * The first token in the accumulation, used for line/column info.
726         */
727        private Token startToken;
728
729        /**
730         * Appends a TEXT token's text to the buffer and tracks the first token.
731         *
732         * @param token the token to accumulate
733         */
734        /* package */ void append(Token token) {
735            if (buffer.isEmpty()) {
736                startToken = token;
737            }
738            buffer.append(token.getText());
739        }
740
741        /**
742         * Flushes the accumulated buffer into a single {@link JavadocNodeImpl} node
743         * and adds it to the given parent. Clears the buffer after flushing.
744         *
745         * @param parent the parent node to add the new node to
746         */
747        /* package */ void flushTo(JavadocNodeImpl parent) {
748            if (!buffer.isEmpty()) {
749                final JavadocNodeImpl startNode = create(startToken);
750                startNode.setText(buffer.toString());
751                parent.addChild(startNode);
752                buffer.setLength(0);
753            }
754        }
755    }
756}