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.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Iterator;
026import java.util.List;
027import java.util.ListIterator;
028import java.util.Optional;
029import java.util.Set;
030
031import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.DetailNode;
034import com.puppycrawl.tools.checkstyle.api.FullIdent;
035import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
036import com.puppycrawl.tools.checkstyle.api.TokenTypes;
037import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
038import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
039import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
041import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
042import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
043
044/**
045 * <div>
046 * Checks the Javadoc of a method or constructor.
047 * </div>
048 *
049 * <p>
050 * Violates parameters and type parameters for which no param tags are present can
051 * be suppressed by defining property {@code allowMissingParamTags}.
052 * </p>
053 *
054 * <p>
055 * Violates methods which return non-void but for which no return tag is present can
056 * be suppressed by defining property {@code allowMissingReturnTag}.
057 * </p>
058 *
059 * <p>
060 * Violates exceptions which are declared to be thrown (by {@code throws} in the method
061 * signature or by {@code throw new} in the method body), but for which no throws tag is
062 * present by activation of property {@code validateThrows}.
063 * Note that {@code throw new} is not checked in the following places:
064 * </p>
065 * <ul>
066 * <li>
067 * Inside a try block (with catch). It is not possible to determine if the thrown
068 * exception can be caught by the catch block as there is no knowledge of the
069 * inheritance hierarchy, so the try block is ignored entirely. However, catch
070 * and finally blocks, as well as try blocks without catch, are still checked.
071 * </li>
072 * <li>
073 * Local classes, anonymous classes and lambda expressions. It is not known when the
074 * throw statements inside such classes are going to be evaluated, so they are ignored.
075 * </li>
076 * </ul>
077 *
078 * <p>
079 * ATTENTION: Checkstyle does not have information about hierarchy of exception types
080 * so usage of base class is considered as separate exception type.
081 * As workaround, you need to specify both types in javadoc (parent and exact type).
082 * </p>
083 *
084 * <p>
085 * Javadoc is not required on a method that is tagged with the {@code @Override}
086 * annotation. However, under Java 5 it is not possible to mark a method required
087 * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle
088 * supports using the convention of using a single {@code {@inheritDoc}} tag
089 * instead of all the other tags.
090 * </p>
091 *
092 * <p>
093 * Note that only inheritable items will allow the {@code {@inheritDoc}}
094 * tag to be used in place of comments. Static methods at all visibilities,
095 * private non-static methods and constructors are not inheritable.
096 * </p>
097 *
098 * <p>
099 * For example, if the following method is implementing a method required by
100 * an interface, then the Javadoc could be done as:
101 * </p>
102 * {@snippet lang="text" :
103 * /** {@inheritDoc} * /
104 * public int checkReturnTag(final int aTagIndex,
105 *                           JavadocTag[] aTags,
106 *                           int aLineNo)
107 * }
108 *
109 * @since 3.0
110 */
111@FileStatefulCheck
112public class JavadocMethodCheck extends AbstractJavadocCheck {
113
114    /**
115     * A key is pointing to the warning message text in "messages.properties" file.
116     */
117    public static final String MSG_CLASS_INFO = "javadoc.classInfo";
118
119    /**
120     * A key is pointing to the warning message text in "messages.properties" file.
121     */
122    public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
123
124    /**
125     * A key is pointing to the warning message text in "messages.properties" file.
126     */
127    public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc";
128
129    /**
130     * A key is pointing to the warning message text in "messages.properties" file.
131     */
132    public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
133
134    /**
135     * A key is pointing to the warning message text in "messages.properties" file.
136     */
137    public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag";
138
139    /**
140     * A key is pointing to the warning message text in "messages.properties" file.
141     */
142    public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected";
143
144    /**
145     * A key is pointing to the warning message text in "messages.properties" file.
146     */
147    public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag";
148
149    /** Html element start symbol. */
150    private static final String ELEMENT_START = "<";
151
152    /** Html element end symbol. */
153    private static final String ELEMENT_END = ">";
154
155    /** Javadoc tags collected from the current Javadoc tree. */
156    private final List<JavadocTag> javadocTags = new ArrayList<>();
157
158    /**
159     * Control whether to allow inline return tags.
160     */
161    private boolean allowInlineReturn;
162
163    /** Specify the access modifiers where Javadoc comments are checked. */
164    private AccessModifierOption[] accessModifiers = {
165        AccessModifierOption.PUBLIC,
166        AccessModifierOption.PROTECTED,
167        AccessModifierOption.PACKAGE,
168        AccessModifierOption.PRIVATE,
169    };
170
171    /**
172     * Control whether to validate {@code throws} tags.
173     */
174    private boolean validateThrows;
175
176    /**
177     * Control whether to ignore violations when a method has parameters but does
178     * not have matching {@code param} tags in the javadoc.
179     */
180    private boolean allowMissingParamTags;
181
182    /**
183     * Control whether to ignore violations when a method returns non-void type
184     * and does not have a {@code return} tag in the javadoc.
185     */
186    private boolean allowMissingReturnTag;
187
188    /** Specify annotations that allow missed documentation. */
189    private Set<String> allowedAnnotations = Set.of("Override");
190
191    /** Java AST node whose attached Javadoc is currently being processed. */
192    private DetailAST currentAst;
193
194    /**
195     * Creates a new {@code JavadocMethodCheck} instance.
196     */
197    public JavadocMethodCheck() {
198        // no code by default
199    }
200
201    /**
202     * Setter to control whether to allow inline return tags.
203     *
204     * @param value a {@code boolean} value
205     * @since 10.23.0
206     */
207    public void setAllowInlineReturn(boolean value) {
208        allowInlineReturn = value;
209    }
210
211    /**
212     * Setter to control whether to validate {@code throws} tags.
213     *
214     * @param value user's value.
215     * @since 6.0
216     */
217    public void setValidateThrows(boolean value) {
218        validateThrows = value;
219    }
220
221    /**
222     * Setter to specify annotations that allow missed documentation.
223     *
224     * @param userAnnotations user's value.
225     * @since 6.0
226     */
227    public void setAllowedAnnotations(String... userAnnotations) {
228        allowedAnnotations = Set.of(userAnnotations);
229    }
230
231    /**
232     * Setter to specify the access modifiers where Javadoc comments are checked.
233     *
234     * @param accessModifiers access modifiers.
235     * @since 8.42
236     */
237    public void setAccessModifiers(AccessModifierOption... accessModifiers) {
238        this.accessModifiers =
239            UnmodifiableCollectionUtil.copyOfArray(accessModifiers, accessModifiers.length);
240    }
241
242    /**
243     * Setter to control whether to ignore violations when a method has parameters
244     * but does not have matching {@code param} tags in the javadoc.
245     *
246     * @param flag a {@code Boolean} value
247     * @since 3.1
248     */
249    public void setAllowMissingParamTags(boolean flag) {
250        allowMissingParamTags = flag;
251    }
252
253    /**
254     * Setter to control whether to ignore violations when a method returns non-void type
255     * and does not have a {@code return} tag in the javadoc.
256     *
257     * @param flag a {@code Boolean} value
258     * @since 3.1
259     */
260    public void setAllowMissingReturnTag(boolean flag) {
261        allowMissingReturnTag = flag;
262    }
263
264    /**
265     * Setter to control when to print violations if the Javadoc being examined by this check
266     * violates the tight html rules defined at
267     * <a href="https://checkstyle.org/writing-javadoc-checks.html#Tight-HTML_rules">
268     *     Tight-HTML Rules</a>.
269     *
270     * @param shouldReportViolation value to which the field shall be set to
271     * @since 8.3
272     * @propertySince 13.7.0
273     */
274    @Override
275    public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
276        super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
277    }
278
279    @Override
280    public final int[] getRequiredTokens() {
281        return CommonUtil.EMPTY_INT_ARRAY;
282    }
283
284    @Override
285    public int[] getDefaultTokens() {
286        return getAcceptableTokens();
287    }
288
289    @Override
290    public int[] getAcceptableTokens() {
291        return new int[] {
292            TokenTypes.METHOD_DEF,
293            TokenTypes.CTOR_DEF,
294            TokenTypes.ANNOTATION_FIELD_DEF,
295            TokenTypes.COMPACT_CTOR_DEF,
296        };
297    }
298
299    @Override
300    public final void visitToken(DetailAST ast) {
301        if (shouldCheck(ast)) {
302            final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
303            if (blockCommentNode != null) {
304                currentAst = ast;
305                super.visitToken(blockCommentNode);
306            }
307        }
308    }
309
310    @Override
311    public void beginJavadocTree(DetailNode rootAst) {
312        javadocTags.clear();
313    }
314
315    @Override
316    public void finishJavadocTree(DetailNode rootAst) {
317        checkCollectedTags();
318    }
319
320    @Override
321    public int[] getDefaultJavadocTokens() {
322        return getRequiredJavadocTokens();
323    }
324
325    @Override
326    public int[] getRequiredJavadocTokens() {
327        return new int[] {
328            JavadocCommentsTokenTypes.PARAM_BLOCK_TAG,
329            JavadocCommentsTokenTypes.RETURN_BLOCK_TAG,
330            JavadocCommentsTokenTypes.RETURN_INLINE_TAG,
331            JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
332            JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
333            JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG,
334        };
335    }
336
337    @Override
338    public void visitJavadocToken(DetailNode ast) {
339        switch (ast.getType()) {
340            case JavadocCommentsTokenTypes.RETURN_BLOCK_TAG -> collectReturn(ast);
341            case JavadocCommentsTokenTypes.RETURN_INLINE_TAG -> {
342                if (allowInlineReturn) {
343                    collectReturn(ast);
344                }
345            }
346            case JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG -> collectInheritDoc();
347            case JavadocCommentsTokenTypes.PARAM_BLOCK_TAG -> collectParam(ast);
348            case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG -> collectThrows(ast, "throws");
349            case JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> collectThrows(ast, "exception");
350            default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
351        }
352    }
353
354    /**
355     * Collects a return tag if it has a description.
356     *
357     * @param ast the return tag node
358     */
359    private void collectReturn(DetailNode ast) {
360        if (JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.DESCRIPTION) != null) {
361            javadocTags.add(new JavadocTag(ast.getLineNumber(), ast.getColumnNumber(), "return"));
362        }
363    }
364
365    /**
366     * Collects an inheritDoc tag.
367     */
368    private void collectInheritDoc() {
369        javadocTags.add(new JavadocTag(0, 0, "inheritDoc"));
370    }
371
372    /**
373     * Collects a param tag.
374     *
375     * @param ast the param tag node
376     */
377    private void collectParam(DetailNode ast) {
378        final DetailNode parameterName = JavadocUtil.findFirstToken(
379                ast, JavadocCommentsTokenTypes.PARAMETER_NAME);
380        if (parameterName != null) {
381            javadocTags.add(new JavadocTag(ast.getLineNumber(), ast.getColumnNumber(),
382                    "param", parameterName.getText()));
383        }
384    }
385
386    /**
387     * Collects a throws or exception tag.
388     *
389     * @param ast the throws or exception tag node
390     * @param tagName the tag name
391     */
392    private void collectThrows(DetailNode ast, String tagName) {
393        final DetailNode identifier = JavadocUtil.findFirstToken(
394                ast, JavadocCommentsTokenTypes.IDENTIFIER);
395        if (identifier != null) {
396            javadocTags.add(new JavadocTag(0, 0,
397                    tagName, identifier.getText()));
398        }
399    }
400
401    /**
402     * Checks collected Javadoc tags against the current AST node.
403     */
404    private void checkCollectedTags() {
405        final List<JavadocTag> tagsToCheck = new ArrayList<>(javadocTags);
406        if (!hasShortCircuitTag(currentAst, tagsToCheck)) {
407            if (currentAst.getType() == TokenTypes.ANNOTATION_FIELD_DEF) {
408                checkReturnTag(tagsToCheck, currentAst.getLineNo(), true);
409            }
410            else {
411                boolean hasInheritDocTag = false;
412                final Iterator<JavadocTag> iterator = tagsToCheck.iterator();
413                while (!hasInheritDocTag && iterator.hasNext()) {
414                    hasInheritDocTag = iterator.next().isInheritDocTag();
415                }
416                final boolean reportExpectedTags = !hasInheritDocTag
417                        && !AnnotationUtil.containsAnnotation(currentAst, allowedAnnotations);
418                if (currentAst.getType() == TokenTypes.COMPACT_CTOR_DEF) {
419                    checkRecordParamTags(tagsToCheck, currentAst, reportExpectedTags);
420                }
421                else {
422                    checkParamTags(tagsToCheck, currentAst, reportExpectedTags);
423                }
424                final List<ExceptionInfo> thrown =
425                        combineExceptionInfo(getThrows(currentAst), getThrowed(currentAst));
426                checkThrowsTags(tagsToCheck, thrown, reportExpectedTags);
427                if (CheckUtil.isNonVoidMethod(currentAst)) {
428                    checkReturnTag(tagsToCheck, currentAst.getLineNo(), reportExpectedTags);
429                }
430            }
431        }
432        tagsToCheck.stream()
433                .filter(javadocTag -> !javadocTag.isInheritDocTag())
434                .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL));
435    }
436
437    /**
438     * Checks whether the given declaration should be validated.
439     *
440     * <p>The declaration is checked only when both its own access modifier and the
441     * access modifier of the surrounding type match the configured
442     * {@code accessModifiers}.</p>
443     *
444     * @param ast the method, constructor, annotation field, or compact constructor
445     *        AST node to check
446     * @return {@code true} if the declaration is inside the configured access scope
447     */
448    private boolean shouldCheck(final DetailAST ast) {
449        final Optional<AccessModifierOption> surroundingAccessModifier = CheckUtil
450                .getSurroundingAccessModifier(ast);
451        final AccessModifierOption accessModifier = CheckUtil
452                .getAccessModifierFromModifiersToken(ast);
453        return surroundingAccessModifier.isPresent() && Arrays.stream(accessModifiers)
454                        .anyMatch(modifier -> modifier == surroundingAccessModifier.get())
455                && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier);
456    }
457
458    /**
459     * Retrieves the list of record components from a given record definition.
460     *
461     * @param recordDef the AST node representing the record definition
462     * @return a list of AST nodes representing the record components
463     */
464    private static List<DetailAST> getRecordComponents(final DetailAST recordDef) {
465        final List<DetailAST> components = new ArrayList<>();
466        final DetailAST recordDecl = recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS);
467
468        DetailAST child = recordDecl.getFirstChild();
469        while (child != null) {
470            if (child.getType() == TokenTypes.RECORD_COMPONENT_DEF) {
471                components.add(child.findFirstToken(TokenTypes.IDENT));
472            }
473            child = child.getNextSibling();
474        }
475        return components;
476    }
477
478    /**
479     * Finds the nearest ancestor record definition node for the given AST node.
480     *
481     * @param ast the AST node to start searching from
482     * @return the nearest {@code RECORD_DEF} AST node, or {@code null} if not found
483     */
484    private static DetailAST getRecordDef(DetailAST ast) {
485        DetailAST current = ast;
486        while (current.getType() != TokenTypes.RECORD_DEF) {
487            current = current.getParent();
488        }
489        return current;
490    }
491
492    /**
493     * Validates whether the Javadoc has a short circuit tag. Currently, this is
494     * the inheritTag. Any violations are logged.
495     *
496     * @param ast the construct being checked
497     * @param tags the list of Javadoc tags associated with the construct
498     * @return true if the construct has a short circuit tag.
499     */
500    private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) {
501        boolean result = true;
502        // Check if it contains {@inheritDoc} tag
503        if (tags.size() == 1
504                && tags.getFirst().isInheritDocTag()) {
505            // Invalid if private, a constructor, or a static method
506            if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) {
507                log(ast, MSG_INVALID_INHERIT_DOC);
508            }
509        }
510        else {
511            result = false;
512        }
513        return result;
514    }
515
516    /**
517     * Computes the parameter nodes for a method.
518     *
519     * @param ast the method node.
520     * @return the list of parameter nodes for ast.
521     */
522    private static List<DetailAST> getParameters(DetailAST ast) {
523        final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
524        final List<DetailAST> returnValue = new ArrayList<>();
525
526        DetailAST child = params.getFirstChild();
527        while (child != null) {
528            final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
529            if (ident != null) {
530                returnValue.add(ident);
531            }
532            child = child.getNextSibling();
533        }
534        return returnValue;
535    }
536
537    /**
538     * Computes the exception nodes for a method.
539     *
540     * @param ast the method node.
541     * @return the list of exception nodes for ast.
542     */
543    private static List<ExceptionInfo> getThrows(DetailAST ast) {
544        final List<ExceptionInfo> returnValue = new ArrayList<>();
545        final DetailAST throwsAST = ast
546                .findFirstToken(TokenTypes.LITERAL_THROWS);
547        if (throwsAST != null) {
548            DetailAST child = throwsAST.getFirstChild();
549            while (child != null) {
550                if (child.getType() == TokenTypes.IDENT
551                        || child.getType() == TokenTypes.DOT) {
552                    returnValue.add(getExceptionInfo(child));
553                }
554                child = child.getNextSibling();
555            }
556        }
557        return returnValue;
558    }
559
560    /**
561     * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'.
562     *
563     * @param methodAst method DetailAST object where to find exceptions
564     * @return list of ExceptionInfo
565     */
566    private static List<ExceptionInfo> getThrowed(DetailAST methodAst) {
567        final List<ExceptionInfo> returnValue = new ArrayList<>();
568        final List<DetailAST> throwLiterals = findTokensInAstByType(methodAst,
569                    TokenTypes.LITERAL_THROW);
570        for (DetailAST throwAst : throwLiterals) {
571            if (!isInIgnoreBlock(methodAst, throwAst)) {
572                final DetailAST newAst = throwAst.getFirstChild().getFirstChild();
573                if (newAst.getType() == TokenTypes.LITERAL_NEW) {
574                    final DetailAST child = newAst.getFirstChild();
575                    returnValue.add(getExceptionInfo(child));
576                }
577            }
578        }
579        return returnValue;
580    }
581
582    /**
583     * Get ExceptionInfo instance.
584     *
585     * @param ast DetailAST object where to find exceptions node;
586     * @return ExceptionInfo
587     */
588    private static ExceptionInfo getExceptionInfo(DetailAST ast) {
589        final FullIdent ident = FullIdent.createFullIdent(ast);
590        final DetailAST firstClassNameNode = getFirstClassNameNode(ast);
591        return new ExceptionInfo(firstClassNameNode,
592                new ClassInfo(new Token(ident)));
593    }
594
595    /**
596     * Get node where class name of exception starts.
597     *
598     * @param ast DetailAST object where to find exceptions node;
599     * @return exception node where class name starts
600     */
601    private static DetailAST getFirstClassNameNode(DetailAST ast) {
602        DetailAST startNode = ast;
603        while (startNode.getType() == TokenTypes.DOT) {
604            startNode = startNode.getFirstChild();
605        }
606        return startNode;
607    }
608
609    /**
610     * Checks if a 'throw' usage is contained within a block that should be ignored.
611     * Such blocks consist of try (with catch) blocks, local classes, anonymous classes,
612     * and lambda expressions. Note that a try block without catch is not considered.
613     *
614     * @param methodBodyAst DetailAST node representing the method body
615     * @param throwAst DetailAST node representing the 'throw' literal
616     * @return true if throwAst is inside a block that should be ignored
617     */
618    private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) {
619        DetailAST ancestor = throwAst;
620        while (ancestor != methodBodyAst) {
621            if (ancestor.getType() == TokenTypes.LAMBDA
622                    || ancestor.getType() == TokenTypes.OBJBLOCK
623                    || ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null) {
624                // throw is inside a lambda expression/anonymous class/local class,
625                // or throw is inside a try block, and there is a catch block
626                break;
627            }
628            if (ancestor.getType() == TokenTypes.LITERAL_CATCH
629                    || ancestor.getType() == TokenTypes.LITERAL_FINALLY) {
630                // if the throw is inside a catch or finally block,
631                // skip the immediate ancestor (try token)
632                ancestor = ancestor.getParent();
633            }
634            ancestor = ancestor.getParent();
635        }
636        return ancestor != methodBodyAst;
637    }
638
639    /**
640     * Combine ExceptionInfo collections together by matching names.
641     *
642     * @param first the first collection of ExceptionInfo
643     * @param second the second collection of ExceptionInfo
644     * @return combined list of ExceptionInfo
645     */
646    private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first,
647                                                            Iterable<ExceptionInfo> second) {
648        final List<ExceptionInfo> result = new ArrayList<>(first);
649        for (ExceptionInfo exceptionInfo : second) {
650            if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) {
651                result.add(exceptionInfo);
652            }
653        }
654        return result;
655    }
656
657    /**
658     * Finds node of specified type among root children, siblings, siblings children
659     * on any deep level.
660     *
661     * @param root    DetailAST
662     * @param astType value of TokenType
663     * @return {@link List} of {@link DetailAST} nodes which matches the predicate.
664     */
665    public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) {
666        final List<DetailAST> result = new ArrayList<>();
667        // iterative preorder depth-first search
668        DetailAST curNode = root;
669        do {
670            // process curNode
671            if (curNode.getType() == astType) {
672                result.add(curNode);
673            }
674            // process children (if any)
675            if (curNode.hasChildren()) {
676                curNode = curNode.getFirstChild();
677                continue;
678            }
679            // backtrack to parent if last child, stopping at root
680            while (curNode.getNextSibling() == null) {
681                curNode = curNode.getParent();
682            }
683            // explore siblings if not root
684            if (curNode != root) {
685                curNode = curNode.getNextSibling();
686            }
687        } while (curNode != root);
688        return result;
689    }
690
691    /**
692     * Checks if all record components in a compact constructor have
693     * corresponding {@code @param} tags.
694     * Reports missing or extra {@code @param} tags in the Javadoc.
695     *
696     * @param tags the list of Javadoc tags
697     * @param compactDef the compact constructor AST node
698     * @param reportExpectedTags whether to report missing {@code @param} tags
699     */
700    private void checkRecordParamTags(final List<JavadocTag> tags,
701        final DetailAST compactDef, boolean reportExpectedTags) {
702
703        final DetailAST parent = getRecordDef(compactDef);
704        final List<DetailAST> params = getRecordComponents(parent);
705
706        for (JavadocTag tag : tags) {
707            if (!tag.isParamTag()) {
708                continue;
709            }
710
711            if (isDuplicateParamTag(tags, tag)) {
712                log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG,
713                        JavadocTagInfo.PARAM.getText());
714            }
715            else {
716                final String arg1 = tag.getFirstArg();
717                final boolean found = removeMatchingParam(params, arg1);
718
719                if (!found) {
720                    log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
721                            JavadocTagInfo.PARAM.getText(), arg1);
722                }
723            }
724        }
725        tags.removeIf(JavadocTag::isParamTag);
726
727        if (!allowMissingParamTags && reportExpectedTags) {
728            for (DetailAST param : params) {
729                log(compactDef, MSG_EXPECTED_TAG,
730                    JavadocTagInfo.PARAM.getText(), param.getText());
731            }
732        }
733    }
734
735    /**
736     * Checks a set of tags for matching parameters.
737     *
738     * @param tags the tags to check
739     * @param parent the node which takes the parameters
740     * @param reportExpectedTags whether we should report if do not find expected tag
741     */
742    private void checkParamTags(final List<JavadocTag> tags,
743            final DetailAST parent, boolean reportExpectedTags) {
744        final List<DetailAST> params = getParameters(parent);
745        final List<DetailAST> typeParams = CheckUtil
746                .getTypeParameters(parent);
747
748        // Loop over the tags, checking to see they exist in the params.
749        for (JavadocTag tag : tags) {
750            if (!tag.isParamTag()) {
751                continue;
752            }
753
754            if (isDuplicateParamTag(tags, tag)) {
755                log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG,
756                        JavadocTagInfo.PARAM.getText());
757            }
758            else {
759                final String arg1 = tag.getFirstArg();
760                boolean found = removeMatchingParam(params, arg1);
761
762                if (arg1.endsWith(ELEMENT_END)) {
763                    found = searchMatchingTypeParameter(typeParams,
764                            arg1.substring(1, arg1.length() - 1));
765                }
766
767                // Handle extra JavadocTag
768                if (!found) {
769                    log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
770                            JavadocTagInfo.PARAM.getText(), arg1);
771                }
772            }
773        }
774        tags.removeIf(JavadocTag::isParamTag);
775
776        // Now dump out all type parameters/parameters without tags :- unless
777        // the user has chosen to suppress these problems
778        if (!allowMissingParamTags && reportExpectedTags) {
779            for (DetailAST param : params) {
780                log(param, MSG_EXPECTED_TAG,
781                    JavadocTagInfo.PARAM.getText(), param.getText());
782            }
783
784            for (DetailAST typeParam : typeParams) {
785                log(typeParam, MSG_EXPECTED_TAG,
786                    JavadocTagInfo.PARAM.getText(),
787                    ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText()
788                    + ELEMENT_END);
789            }
790        }
791    }
792
793    /**
794     * Checks if the {@code @param} tag duplicates an earlier {@code @param} tag.
795     *
796     * @param tags all tags to check
797     * @param tag the tag to check
798     * @return true if there is an earlier {@code @param} tag with the same argument
799     */
800    private static boolean isDuplicateParamTag(List<JavadocTag> tags, JavadocTag tag) {
801        final int tagIndex = tags.indexOf(tag);
802        boolean result = false;
803        for (int tagPosition = 0; tagPosition < tagIndex; tagPosition++) {
804            final JavadocTag currentTag = tags.get(tagPosition);
805            if (currentTag.isParamTag()
806                    && currentTag.getFirstArg().equals(tag.getFirstArg())) {
807                result = true;
808            }
809        }
810        return result;
811    }
812
813    /**
814     * Returns true if required type found in type parameters.
815     *
816     * @param typeParams
817     *            collection of type parameters
818     * @param requiredTypeName
819     *            name of required type
820     * @return true if required type found in type parameters.
821     */
822    private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams,
823            String requiredTypeName) {
824        // Loop looking for matching type param
825        final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
826        boolean found = false;
827        while (typeParamsIt.hasNext()) {
828            final DetailAST typeParam = typeParamsIt.next();
829            if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
830                    .equals(requiredTypeName)) {
831                found = true;
832                typeParamsIt.remove();
833                break;
834            }
835        }
836        return found;
837    }
838
839    /**
840     * Remove parameter from params collection by name.
841     *
842     * @param params collection of DetailAST parameters
843     * @param paramName name of parameter
844     * @return true if parameter found and removed
845     */
846    private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) {
847        boolean found = false;
848        final Iterator<DetailAST> paramIt = params.iterator();
849        while (paramIt.hasNext()) {
850            final DetailAST param = paramIt.next();
851            if (param.getText().equals(paramName)) {
852                found = true;
853                paramIt.remove();
854                break;
855            }
856        }
857        return found;
858    }
859
860    /**
861     * Checks for only one return tag. All return tags will be removed from the supplied list.
862     *
863     * @param tags the tags to check
864     * @param lineNo the line number of the expected tag
865     * @param reportExpectedTags whether we should report if do not find expected tag
866     */
867    private void checkReturnTag(List<JavadocTag> tags, int lineNo,
868        boolean reportExpectedTags) {
869        // Loop over tags finding return tags. After the first one, report a violation
870        boolean found = false;
871        final ListIterator<JavadocTag> it = tags.listIterator();
872        while (it.hasNext()) {
873            final JavadocTag javadocTag = it.next();
874            if (javadocTag.isReturnTag()) {
875                if (found) {
876                    log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
877                            MSG_DUPLICATE_TAG,
878                            JavadocTagInfo.RETURN.getText());
879                }
880                found = true;
881                it.remove();
882            }
883        }
884
885        // Handle there being no @return tags :- unless
886        // the user has chosen to suppress these problems
887        if (!found && !allowMissingReturnTag && reportExpectedTags) {
888            log(lineNo, MSG_RETURN_EXPECTED);
889        }
890    }
891
892    /**
893     * Checks a set of tags for matching throws.
894     *
895     * @param tags the tags to check
896     * @param throwsList the throws to check
897     * @param reportExpectedTags whether we should report if do not find expected tag
898     */
899    private void checkThrowsTags(List<JavadocTag> tags,
900            List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
901        // Loop over the tags, checking to see they exist in the throws.
902        final ListIterator<JavadocTag> tagIt = tags.listIterator();
903        while (tagIt.hasNext()) {
904            final JavadocTag tag = tagIt.next();
905
906            if (!tag.isThrowsTag()) {
907                continue;
908            }
909            tagIt.remove();
910
911            // Loop looking for matching throw
912            processThrows(throwsList, tag.getFirstArg());
913        }
914        // Now dump out all throws without tags :- unless
915        // the user has chosen to suppress these problems
916        if (validateThrows && reportExpectedTags) {
917            throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
918                .forEach(exceptionInfo -> {
919                    final Token token = exceptionInfo.getName();
920                    log(exceptionInfo.getAst(),
921                        MSG_EXPECTED_TAG,
922                        JavadocTagInfo.THROWS.getText(), token.text());
923                });
924        }
925    }
926
927    /**
928     * Verifies that documented exception is in throws.
929     *
930     * @param throwsIterable collection of throws
931     * @param documentedClassName documented exception class name
932     */
933    private static void processThrows(Iterable<ExceptionInfo> throwsIterable,
934                                      String documentedClassName) {
935        for (ExceptionInfo exceptionInfo : throwsIterable) {
936            if (isClassNamesSame(exceptionInfo.getName().text(),
937                    documentedClassName)) {
938                exceptionInfo.setFound();
939                break;
940            }
941        }
942    }
943
944    /**
945     * Check that ExceptionInfo objects are same by name.
946     *
947     * @param info1 ExceptionInfo object
948     * @param info2 ExceptionInfo object
949     * @return true is ExceptionInfo object have the same name
950     */
951    private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) {
952        return isClassNamesSame(info1.getName().text(),
953                                    info2.getName().text());
954    }
955
956    /**
957     * Check that class names are same by short name of class. If some class name is fully
958     * qualified it is cut to short name.
959     *
960     * @param class1 class name
961     * @param class2 class name
962     * @return true is ExceptionInfo object have the same name
963     */
964    private static boolean isClassNamesSame(String class1, String class2) {
965        final String class1ShortName = class1
966                .substring(class1.lastIndexOf('.') + 1);
967        final String class2ShortName = class2
968                .substring(class2.lastIndexOf('.') + 1);
969        return class1ShortName.equals(class2ShortName);
970    }
971
972    /**
973     * Contains class's {@code Token}.
974     *
975     * @param name {@code FullIdent} associated with this class.
976     */
977    private record ClassInfo(Token name) {
978    }
979
980    /**
981     * Represents text element with location in the text.
982     *
983     * @param text Token's text.
984     */
985    private record Token(String text) {
986
987        /**
988         * Converts FullIdent to Token.
989         *
990         * @param fullIdent full ident to convert.
991         */
992        private Token(FullIdent fullIdent) {
993            this(fullIdent.getText());
994        }
995    }
996
997    /** Stores useful information about declared exception. */
998    private static final class ExceptionInfo {
999
1000        /** AST node representing this exception. */
1001        private final DetailAST ast;
1002
1003        /** Class information associated with this exception. */
1004        private final ClassInfo classInfo;
1005        /** Does the exception have throws tag associated with. */
1006        private boolean found;
1007
1008        /**
1009         * Creates new instance for {@code FullIdent}.
1010         *
1011         * @param ast AST node representing this exception
1012         * @param classInfo class info
1013         */
1014        private ExceptionInfo(DetailAST ast, ClassInfo classInfo) {
1015            this.ast = ast;
1016            this.classInfo = classInfo;
1017        }
1018
1019        /**
1020         * Gets the AST node representing this exception.
1021         *
1022         * @return the AST node representing this exception
1023         */
1024        private DetailAST getAst() {
1025            return ast;
1026        }
1027
1028        /** Mark that the exception has associated throws tag. */
1029        private void setFound() {
1030            found = true;
1031        }
1032
1033        /**
1034         * Checks that the exception has throws tag associated with it.
1035         *
1036         * @return whether the exception has throws tag associated with
1037         */
1038        private boolean isFound() {
1039            return found;
1040        }
1041
1042        /**
1043         * Gets exception name.
1044         *
1045         * @return exception's name
1046         */
1047        private Token getName() {
1048            return classInfo.name();
1049        }
1050
1051    }
1052
1053}