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
155        if (tag instanceof ParserRuleContext context) {
156            final Token tagName = (Token) context.getChild(1).getPayload();
157            final int tokenType = tagName.getType();
158
159            final JavadocNodeImpl specificTagNode = switch (tokenType) {
160                case JavadocCommentsLexer.AUTHOR ->
161                    buildImaginaryNode(JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG, ctx);
162                case JavadocCommentsLexer.DEPRECATED ->
163                    buildImaginaryNode(JavadocCommentsTokenTypes.DEPRECATED_BLOCK_TAG, ctx);
164                case JavadocCommentsLexer.RETURN ->
165                    buildImaginaryNode(JavadocCommentsTokenTypes.RETURN_BLOCK_TAG, ctx);
166                case JavadocCommentsLexer.PARAM ->
167                    buildImaginaryNode(JavadocCommentsTokenTypes.PARAM_BLOCK_TAG, ctx);
168                case JavadocCommentsLexer.THROWS ->
169                    buildImaginaryNode(JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, ctx);
170                case JavadocCommentsLexer.EXCEPTION ->
171                    buildImaginaryNode(JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG, ctx);
172                case JavadocCommentsLexer.SINCE ->
173                    buildImaginaryNode(JavadocCommentsTokenTypes.SINCE_BLOCK_TAG, ctx);
174                case JavadocCommentsLexer.VERSION ->
175                    buildImaginaryNode(JavadocCommentsTokenTypes.VERSION_BLOCK_TAG, ctx);
176                case JavadocCommentsLexer.SEE ->
177                    buildImaginaryNode(JavadocCommentsTokenTypes.SEE_BLOCK_TAG, ctx);
178                case JavadocCommentsLexer.LITERAL_HIDDEN ->
179                    buildImaginaryNode(JavadocCommentsTokenTypes.HIDDEN_BLOCK_TAG, ctx);
180                case JavadocCommentsLexer.USES ->
181                    buildImaginaryNode(JavadocCommentsTokenTypes.USES_BLOCK_TAG, ctx);
182                case JavadocCommentsLexer.PROVIDES ->
183                    buildImaginaryNode(JavadocCommentsTokenTypes.PROVIDES_BLOCK_TAG, ctx);
184                case JavadocCommentsLexer.SERIAL ->
185                    buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_BLOCK_TAG, ctx);
186                case JavadocCommentsLexer.SERIAL_DATA ->
187                    buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_DATA_BLOCK_TAG, ctx);
188                case JavadocCommentsLexer.SERIAL_FIELD ->
189                    buildImaginaryNode(JavadocCommentsTokenTypes.SERIAL_FIELD_BLOCK_TAG, ctx);
190                default ->
191                    buildImaginaryNode(JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG, ctx);
192            };
193            blockTagNode.addChild(specificTagNode);
194        }
195
196        return blockTagNode;
197    }
198
199    @Override
200    public JavadocNodeImpl visitAuthorTag(JavadocCommentsParser.AuthorTagContext ctx) {
201        return flattenedTree(ctx);
202    }
203
204    @Override
205    public JavadocNodeImpl visitDeprecatedTag(JavadocCommentsParser.DeprecatedTagContext ctx) {
206        return flattenedTree(ctx);
207    }
208
209    @Override
210    public JavadocNodeImpl visitReturnTag(JavadocCommentsParser.ReturnTagContext ctx) {
211        return flattenedTree(ctx);
212    }
213
214    @Override
215    public JavadocNodeImpl visitParameterTag(JavadocCommentsParser.ParameterTagContext ctx) {
216        return flattenedTree(ctx);
217    }
218
219    @Override
220    public JavadocNodeImpl visitThrowsTag(JavadocCommentsParser.ThrowsTagContext ctx) {
221        return flattenedTree(ctx);
222    }
223
224    @Override
225    public JavadocNodeImpl visitExceptionTag(JavadocCommentsParser.ExceptionTagContext ctx) {
226        return flattenedTree(ctx);
227    }
228
229    @Override
230    public JavadocNodeImpl visitSinceTag(JavadocCommentsParser.SinceTagContext ctx) {
231        return flattenedTree(ctx);
232    }
233
234    @Override
235    public JavadocNodeImpl visitVersionTag(JavadocCommentsParser.VersionTagContext ctx) {
236        return flattenedTree(ctx);
237    }
238
239    @Override
240    public JavadocNodeImpl visitSeeTag(JavadocCommentsParser.SeeTagContext ctx) {
241        return flattenedTree(ctx);
242    }
243
244    @Override
245    public JavadocNodeImpl visitHiddenTag(JavadocCommentsParser.HiddenTagContext ctx) {
246        return flattenedTree(ctx);
247    }
248
249    @Override
250    public JavadocNodeImpl visitUsesTag(JavadocCommentsParser.UsesTagContext ctx) {
251        return flattenedTree(ctx);
252    }
253
254    @Override
255    public JavadocNodeImpl visitProvidesTag(JavadocCommentsParser.ProvidesTagContext ctx) {
256        return flattenedTree(ctx);
257    }
258
259    @Override
260    public JavadocNodeImpl visitSerialTag(JavadocCommentsParser.SerialTagContext ctx) {
261        return flattenedTree(ctx);
262    }
263
264    @Override
265    public JavadocNodeImpl visitSerialDataTag(JavadocCommentsParser.SerialDataTagContext ctx) {
266        return flattenedTree(ctx);
267    }
268
269    @Override
270    public JavadocNodeImpl visitSerialFieldTag(JavadocCommentsParser.SerialFieldTagContext ctx) {
271        return flattenedTree(ctx);
272    }
273
274    @Override
275    public JavadocNodeImpl visitCustomBlockTag(JavadocCommentsParser.CustomBlockTagContext ctx) {
276        return flattenedTree(ctx);
277    }
278
279    @Override
280    public JavadocNodeImpl visitInlineTag(JavadocCommentsParser.InlineTagContext ctx) {
281        final JavadocNodeImpl inlineTagNode =
282                createImaginary(JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG);
283        final ParseTree tagContent = ctx.inlineTagContent().getChild(0);
284
285        if (tagContent instanceof ParserRuleContext context) {
286            final Token tagName = (Token) context.getChild(0).getPayload();
287            final int tokenType = tagName.getType();
288
289            final JavadocNodeImpl specificTagNode = switch (tokenType) {
290                case JavadocCommentsLexer.CODE ->
291                    buildImaginaryNode(JavadocCommentsTokenTypes.CODE_INLINE_TAG, ctx);
292                case JavadocCommentsLexer.LINK ->
293                    buildImaginaryNode(JavadocCommentsTokenTypes.LINK_INLINE_TAG, ctx);
294                case JavadocCommentsLexer.LINKPLAIN ->
295                    buildImaginaryNode(JavadocCommentsTokenTypes.LINKPLAIN_INLINE_TAG, ctx);
296                case JavadocCommentsLexer.VALUE ->
297                    buildImaginaryNode(JavadocCommentsTokenTypes.VALUE_INLINE_TAG, ctx);
298                case JavadocCommentsLexer.INHERIT_DOC ->
299                    buildImaginaryNode(JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG, ctx);
300                case JavadocCommentsLexer.SUMMARY ->
301                    buildImaginaryNode(JavadocCommentsTokenTypes.SUMMARY_INLINE_TAG, ctx);
302                case JavadocCommentsLexer.SYSTEM_PROPERTY ->
303                    buildImaginaryNode(JavadocCommentsTokenTypes.SYSTEM_PROPERTY_INLINE_TAG, ctx);
304                case JavadocCommentsLexer.INDEX ->
305                    buildImaginaryNode(JavadocCommentsTokenTypes.INDEX_INLINE_TAG, ctx);
306                case JavadocCommentsLexer.RETURN ->
307                    buildImaginaryNode(JavadocCommentsTokenTypes.RETURN_INLINE_TAG, ctx);
308                case JavadocCommentsLexer.LITERAL ->
309                    buildImaginaryNode(JavadocCommentsTokenTypes.LITERAL_INLINE_TAG, ctx);
310                case JavadocCommentsLexer.SNIPPET ->
311                    buildImaginaryNode(JavadocCommentsTokenTypes.SNIPPET_INLINE_TAG, ctx);
312                default -> buildImaginaryNode(JavadocCommentsTokenTypes.CUSTOM_INLINE_TAG, ctx);
313            };
314            inlineTagNode.addChild(specificTagNode);
315        }
316
317        return inlineTagNode;
318    }
319
320    @Override
321    public JavadocNodeImpl visitInlineTagContent(
322            JavadocCommentsParser.InlineTagContentContext ctx) {
323        return flattenedTree(ctx);
324    }
325
326    @Override
327    public JavadocNodeImpl visitCodeInlineTag(JavadocCommentsParser.CodeInlineTagContext ctx) {
328        return flattenedTree(ctx);
329    }
330
331    @Override
332    public JavadocNodeImpl visitLinkPlainInlineTag(
333            JavadocCommentsParser.LinkPlainInlineTagContext ctx) {
334        return flattenedTree(ctx);
335    }
336
337    @Override
338    public JavadocNodeImpl visitLinkInlineTag(JavadocCommentsParser.LinkInlineTagContext ctx) {
339        return flattenedTree(ctx);
340    }
341
342    @Override
343    public JavadocNodeImpl visitValueInlineTag(JavadocCommentsParser.ValueInlineTagContext ctx) {
344        return flattenedTree(ctx);
345    }
346
347    @Override
348    public JavadocNodeImpl visitInheritDocInlineTag(
349            JavadocCommentsParser.InheritDocInlineTagContext ctx) {
350        return flattenedTree(ctx);
351    }
352
353    @Override
354    public JavadocNodeImpl visitSummaryInlineTag(
355            JavadocCommentsParser.SummaryInlineTagContext ctx) {
356        return flattenedTree(ctx);
357    }
358
359    @Override
360    public JavadocNodeImpl visitSystemPropertyInlineTag(
361            JavadocCommentsParser.SystemPropertyInlineTagContext ctx) {
362        return flattenedTree(ctx);
363    }
364
365    @Override
366    public JavadocNodeImpl visitIndexInlineTag(JavadocCommentsParser.IndexInlineTagContext ctx) {
367        return flattenedTree(ctx);
368    }
369
370    @Override
371    public JavadocNodeImpl visitReturnInlineTag(JavadocCommentsParser.ReturnInlineTagContext ctx) {
372        return flattenedTree(ctx);
373    }
374
375    @Override
376    public JavadocNodeImpl visitLiteralInlineTag(
377            JavadocCommentsParser.LiteralInlineTagContext ctx) {
378        return flattenedTree(ctx);
379    }
380
381    @Override
382    public JavadocNodeImpl visitSnippetInlineTag(
383            JavadocCommentsParser.SnippetInlineTagContext ctx) {
384        final JavadocNodeImpl dummyRoot = new JavadocNodeImpl();
385        if (!ctx.snippetAttributes.isEmpty()) {
386            final JavadocNodeImpl snippetAttributes =
387                    createImaginary(JavadocCommentsTokenTypes.SNIPPET_ATTRIBUTES);
388            ctx.snippetAttributes.forEach(snippetAttributeContext -> {
389                final JavadocNodeImpl snippetAttribute = visit(snippetAttributeContext);
390                snippetAttributes.addChild(snippetAttribute);
391            });
392            dummyRoot.addChild(snippetAttributes);
393        }
394        if (ctx.COLON() != null) {
395            dummyRoot.addChild(create((Token) ctx.COLON().getPayload()));
396        }
397        if (ctx.snippetBody() != null) {
398            dummyRoot.addChild(visit(ctx.snippetBody()));
399        }
400        return dummyRoot.getFirstChild();
401    }
402
403    @Override
404    public JavadocNodeImpl visitCustomInlineTag(JavadocCommentsParser.CustomInlineTagContext ctx) {
405        return flattenedTree(ctx);
406    }
407
408    @Override
409    public JavadocNodeImpl visitReference(JavadocCommentsParser.ReferenceContext ctx) {
410        return buildImaginaryNode(JavadocCommentsTokenTypes.REFERENCE, ctx);
411    }
412
413    @Override
414    public JavadocNodeImpl visitTypeName(JavadocCommentsParser.TypeNameContext ctx) {
415        return flattenedTree(ctx);
416
417    }
418
419    @Override
420    public JavadocNodeImpl visitQualifiedName(JavadocCommentsParser.QualifiedNameContext ctx) {
421        return flattenedTree(ctx);
422    }
423
424    @Override
425    public JavadocNodeImpl visitTypeArguments(JavadocCommentsParser.TypeArgumentsContext ctx) {
426        return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENTS, ctx);
427    }
428
429    @Override
430    public JavadocNodeImpl visitTypeArgument(JavadocCommentsParser.TypeArgumentContext ctx) {
431        return buildImaginaryNode(JavadocCommentsTokenTypes.TYPE_ARGUMENT, ctx);
432    }
433
434    @Override
435    public JavadocNodeImpl visitMemberReference(JavadocCommentsParser.MemberReferenceContext 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                    accumulator.flushTo(parent);
608                    parent.addChild(create(token));
609                }
610            }
611            else {
612                accumulator.flushTo(parent);
613                final Token token = ((ParserRuleContext) child).getStart();
614                addHiddenTokensToTheLeft(token, parent);
615                parent.addChild(visit(child));
616            }
617        }
618
619        accumulator.flushTo(parent);
620    }
621
622    /**
623     * Checks whether a token is a Javadoc text token.
624     *
625     * @param token the token to check
626     * @return true if the token is a text token, false otherwise
627     */
628    private static boolean isTextToken(Token token) {
629        return token.getType() == JavadocCommentsTokenTypes.TEXT;
630    }
631
632    /**
633     * Adds hidden tokens to the left of the given token to the parent node.
634     * Ensures text accumulation is flushed before adding hidden tokens.
635     * Hidden tokens are only added once per unique token index.
636     *
637     * @param token      the token whose hidden tokens should be added
638     * @param parent     the parent node to which hidden tokens are added
639     */
640    private void addHiddenTokensToTheLeft(Token token, JavadocNodeImpl parent) {
641        final boolean alreadyProcessed = !processedTokenIndices.add(token.getTokenIndex());
642
643        if (!alreadyProcessed) {
644            final int tokenIndex = token.getTokenIndex();
645            final List<Token> hiddenTokens = tokens.getHiddenTokensToLeft(tokenIndex);
646            if (hiddenTokens != null) {
647                accumulator.flushTo(parent);
648                for (Token hiddenToken : hiddenTokens) {
649                    parent.addChild(create(hiddenToken));
650                }
651            }
652        }
653    }
654
655    /**
656     * Creates a JavadocNodeImpl from the given token.
657     *
658     * @param token the token to create the JavadocNodeImpl from
659     * @return a new JavadocNodeImpl initialized with the token
660     */
661    private JavadocNodeImpl create(Token token) {
662        final JavadocNodeImpl node = new JavadocNodeImpl();
663        node.initialize(token);
664
665        // adjust line number to the position of the block comment
666        node.setLineNumber(node.getLineNumber() + blockCommentLineNumber);
667
668        // adjust first line to indent of /**
669        if (node.getLineNumber() == blockCommentLineNumber) {
670            node.setColumnNumber(node.getColumnNumber() + javadocColumnNumber);
671        }
672
673        if (isJavadocTag(token.getType())) {
674            node.setType(JavadocCommentsTokenTypes.TAG_NAME);
675        }
676
677        if (token.getType() == JavadocCommentsLexer.WS) {
678            node.setType(JavadocCommentsTokenTypes.TEXT);
679        }
680
681        return node;
682    }
683
684    /**
685     * Checks if the given token type is a Javadoc tag.
686     *
687     * @param type the token type to check
688     * @return true if the token type is a Javadoc tag, false otherwise
689     */
690    private static boolean isJavadocTag(int type) {
691        return JAVADOC_TAG_TYPES.contains(type);
692    }
693
694    /**
695     * Create a JavadocNodeImpl from a given token and token type. This method
696     * should be used for imaginary nodes only, i.e. 'JAVADOC_INLINE_TAG -&gt; JAVADOC_INLINE_TAG',
697     * where the text on the RHS matches the text on the LHS.
698     *
699     * @param tokenType the token type of this JavadocNodeImpl
700     * @return new JavadocNodeImpl of given type
701     */
702    private JavadocNodeImpl createImaginary(int tokenType) {
703        final JavadocNodeImpl node = new JavadocNodeImpl();
704        node.setType(tokenType);
705        node.setText(JavadocUtil.getTokenName(tokenType));
706
707        if (tokenType == JavadocCommentsTokenTypes.JAVADOC_CONTENT) {
708            node.setLineNumber(blockCommentLineNumber);
709            node.setColumnNumber(javadocColumnNumber);
710        }
711
712        return node;
713    }
714
715    /**
716     * Returns the first non-tight HTML tag encountered in the Javadoc comment, if any.
717     *
718     * @return the first non-tight HTML tag, or null if none was found
719     */
720    public DetailNode getFirstNonTightHtmlTag() {
721        return firstNonTightHtmlTag;
722    }
723
724    /**
725     * A small utility to accumulate consecutive TEXT tokens into one node,
726     * preserving the starting token for accurate location metadata.
727     */
728    private final class TextAccumulator {
729        /**
730         * Buffer to accumulate TEXT token texts.
731         *
732         * @noinspection StringBufferField
733         * @noinspectionreason StringBufferField - We want to reuse the same buffer to avoid
734         */
735        private final StringBuilder buffer = new StringBuilder(256);
736
737        /**
738         * The first token in the accumulation, used for line/column info.
739         */
740        private Token startToken;
741
742        /**
743         * Appends a TEXT token's text to the buffer and tracks the first token.
744         *
745         * @param token the token to accumulate
746         */
747        /* package */ void append(Token token) {
748            if (buffer.isEmpty()) {
749                startToken = token;
750            }
751            buffer.append(token.getText());
752        }
753
754        /**
755         * Flushes the accumulated buffer into a single {@link JavadocNodeImpl} node
756         * and adds it to the given parent. Clears the buffer after flushing.
757         *
758         * @param parent the parent node to add the new node to
759         */
760        /* package */ void flushTo(JavadocNodeImpl parent) {
761            if (!buffer.isEmpty()) {
762                final JavadocNodeImpl startNode = create(startToken);
763                startNode.setText(buffer.toString());
764                parent.addChild(startNode);
765                buffer.setLength(0);
766            }
767        }
768    }
769}