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