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