View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.ListIterator;
28  import java.util.Optional;
29  import java.util.Set;
30  
31  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.DetailNode;
34  import com.puppycrawl.tools.checkstyle.api.FullIdent;
35  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
38  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
39  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
40  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
42  import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
43  
44  /**
45   * <div>
46   * Checks the Javadoc of a method or constructor.
47   * </div>
48   *
49   * <p>
50   * Violates parameters and type parameters for which no param tags are present can
51   * be suppressed by defining property {@code allowMissingParamTags}.
52   * </p>
53   *
54   * <p>
55   * Violates methods which return non-void but for which no return tag is present can
56   * be suppressed by defining property {@code allowMissingReturnTag}.
57   * </p>
58   *
59   * <p>
60   * Violates exceptions which are declared to be thrown (by {@code throws} in the method
61   * signature or by {@code throw new} in the method body), but for which no throws tag is
62   * present by activation of property {@code validateThrows}.
63   * Note that {@code throw new} is not checked in the following places:
64   * </p>
65   * <ul>
66   * <li>
67   * Inside a try block (with catch). It is not possible to determine if the thrown
68   * exception can be caught by the catch block as there is no knowledge of the
69   * inheritance hierarchy, so the try block is ignored entirely. However, catch
70   * and finally blocks, as well as try blocks without catch, are still checked.
71   * </li>
72   * <li>
73   * Local classes, anonymous classes and lambda expressions. It is not known when the
74   * throw statements inside such classes are going to be evaluated, so they are ignored.
75   * </li>
76   * </ul>
77   *
78   * <p>
79   * ATTENTION: Checkstyle does not have information about hierarchy of exception types
80   * so usage of base class is considered as separate exception type.
81   * As workaround, you need to specify both types in javadoc (parent and exact type).
82   * </p>
83   *
84   * <p>
85   * Javadoc is not required on a method that is tagged with the {@code @Override}
86   * annotation. However, under Java 5 it is not possible to mark a method required
87   * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle
88   * supports using the convention of using a single {@code {@inheritDoc}} tag
89   * instead of all the other tags.
90   * </p>
91   *
92   * <p>
93   * Note that only inheritable items will allow the {@code {@inheritDoc}}
94   * tag to be used in place of comments. Static methods at all visibilities,
95   * private non-static methods and constructors are not inheritable.
96   * </p>
97   *
98   * <p>
99   * 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
112 public 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      * 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/writingjavadocchecks.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         final ListIterator<JavadocTag> tagIt = tags.listIterator();
707         while (tagIt.hasNext()) {
708             final JavadocTag tag = tagIt.next();
709 
710             if (!tag.isParamTag()) {
711                 continue;
712             }
713 
714             tagIt.remove();
715 
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         if (!allowMissingParamTags && reportExpectedTags) {
726             for (DetailAST param : params) {
727                 log(compactDef, MSG_EXPECTED_TAG,
728                     JavadocTagInfo.PARAM.getText(), param.getText());
729             }
730         }
731     }
732 
733     /**
734      * Checks a set of tags for matching parameters.
735      *
736      * @param tags the tags to check
737      * @param parent the node which takes the parameters
738      * @param reportExpectedTags whether we should report if do not find
739      *            expected tag
740      */
741     private void checkParamTags(final List<JavadocTag> tags,
742             final DetailAST parent, boolean reportExpectedTags) {
743         final List<DetailAST> params = getParameters(parent);
744         final List<DetailAST> typeParams = CheckUtil
745                 .getTypeParameters(parent);
746 
747         // Loop over the tags, checking to see they exist in the params.
748         final ListIterator<JavadocTag> tagIt = tags.listIterator();
749         while (tagIt.hasNext()) {
750             final JavadocTag tag = tagIt.next();
751 
752             if (!tag.isParamTag()) {
753                 continue;
754             }
755 
756             tagIt.remove();
757 
758             final String arg1 = tag.getFirstArg();
759             boolean found = removeMatchingParam(params, arg1);
760 
761             if (arg1.endsWith(ELEMENT_END)) {
762                 found = searchMatchingTypeParameter(typeParams,
763                         arg1.substring(1, arg1.length() - 1));
764             }
765 
766             // Handle extra JavadocTag
767             if (!found) {
768                 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
769                         JavadocTagInfo.PARAM.getText(), arg1);
770             }
771         }
772 
773         // Now dump out all type parameters/parameters without tags :- unless
774         // the user has chosen to suppress these problems
775         if (!allowMissingParamTags && reportExpectedTags) {
776             for (DetailAST param : params) {
777                 log(param, MSG_EXPECTED_TAG,
778                     JavadocTagInfo.PARAM.getText(), param.getText());
779             }
780 
781             for (DetailAST typeParam : typeParams) {
782                 log(typeParam, MSG_EXPECTED_TAG,
783                     JavadocTagInfo.PARAM.getText(),
784                     ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText()
785                     + ELEMENT_END);
786             }
787         }
788     }
789 
790     /**
791      * Returns true if required type found in type parameters.
792      *
793      * @param typeParams
794      *            collection of type parameters
795      * @param requiredTypeName
796      *            name of required type
797      * @return true if required type found in type parameters.
798      */
799     private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams,
800             String requiredTypeName) {
801         // Loop looking for matching type param
802         final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
803         boolean found = false;
804         while (typeParamsIt.hasNext()) {
805             final DetailAST typeParam = typeParamsIt.next();
806             if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
807                     .equals(requiredTypeName)) {
808                 found = true;
809                 typeParamsIt.remove();
810                 break;
811             }
812         }
813         return found;
814     }
815 
816     /**
817      * Remove parameter from params collection by name.
818      *
819      * @param params collection of DetailAST parameters
820      * @param paramName name of parameter
821      * @return true if parameter found and removed
822      */
823     private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) {
824         boolean found = false;
825         final Iterator<DetailAST> paramIt = params.iterator();
826         while (paramIt.hasNext()) {
827             final DetailAST param = paramIt.next();
828             if (param.getText().equals(paramName)) {
829                 found = true;
830                 paramIt.remove();
831                 break;
832             }
833         }
834         return found;
835     }
836 
837     /**
838      * Checks for only one return tag. All return tags will be removed from the
839      * supplied list.
840      *
841      * @param tags the tags to check
842      * @param lineNo the line number of the expected tag
843      * @param reportExpectedTags whether we should report if do not find
844      *            expected tag
845      */
846     private void checkReturnTag(List<JavadocTag> tags, int lineNo,
847         boolean reportExpectedTags) {
848         // Loop over tags finding return tags. After the first one, report a violation
849         boolean found = false;
850         final ListIterator<JavadocTag> it = tags.listIterator();
851         while (it.hasNext()) {
852             final JavadocTag javadocTag = it.next();
853             if (javadocTag.isReturnTag()) {
854                 if (found) {
855                     log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
856                             MSG_DUPLICATE_TAG,
857                             JavadocTagInfo.RETURN.getText());
858                 }
859                 found = true;
860                 it.remove();
861             }
862         }
863 
864         // Handle there being no @return tags :- unless
865         // the user has chosen to suppress these problems
866         if (!found && !allowMissingReturnTag && reportExpectedTags) {
867             log(lineNo, MSG_RETURN_EXPECTED);
868         }
869     }
870 
871     /**
872      * Checks a set of tags for matching throws.
873      *
874      * @param tags the tags to check
875      * @param throwsList the throws to check
876      * @param reportExpectedTags whether we should report if do not find
877      *            expected tag
878      */
879     private void checkThrowsTags(List<JavadocTag> tags,
880             List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
881         // Loop over the tags, checking to see they exist in the throws.
882         final ListIterator<JavadocTag> tagIt = tags.listIterator();
883         while (tagIt.hasNext()) {
884             final JavadocTag tag = tagIt.next();
885 
886             if (!tag.isThrowsTag()) {
887                 continue;
888             }
889             tagIt.remove();
890 
891             // Loop looking for matching throw
892             processThrows(throwsList, tag.getFirstArg());
893         }
894         // Now dump out all throws without tags :- unless
895         // the user has chosen to suppress these problems
896         if (validateThrows && reportExpectedTags) {
897             throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
898                 .forEach(exceptionInfo -> {
899                     final Token token = exceptionInfo.getName();
900                     log(exceptionInfo.getAst(),
901                         MSG_EXPECTED_TAG,
902                         JavadocTagInfo.THROWS.getText(), token.text());
903                 });
904         }
905     }
906 
907     /**
908      * Verifies that documented exception is in throws.
909      *
910      * @param throwsIterable collection of throws
911      * @param documentedClassName documented exception class name
912      */
913     private static void processThrows(Iterable<ExceptionInfo> throwsIterable,
914                                       String documentedClassName) {
915         for (ExceptionInfo exceptionInfo : throwsIterable) {
916             if (isClassNamesSame(exceptionInfo.getName().text(),
917                     documentedClassName)) {
918                 exceptionInfo.setFound();
919                 break;
920             }
921         }
922     }
923 
924     /**
925      * Check that ExceptionInfo objects are same by name.
926      *
927      * @param info1 ExceptionInfo object
928      * @param info2 ExceptionInfo object
929      * @return true is ExceptionInfo object have the same name
930      */
931     private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) {
932         return isClassNamesSame(info1.getName().text(),
933                                     info2.getName().text());
934     }
935 
936     /**
937      * Check that class names are same by short name of class. If some class name is fully
938      * qualified it is cut to short name.
939      *
940      * @param class1 class name
941      * @param class2 class name
942      * @return true is ExceptionInfo object have the same name
943      */
944     private static boolean isClassNamesSame(String class1, String class2) {
945         final String class1ShortName = class1
946                 .substring(class1.lastIndexOf('.') + 1);
947         final String class2ShortName = class2
948                 .substring(class2.lastIndexOf('.') + 1);
949         return class1ShortName.equals(class2ShortName);
950     }
951 
952     /**
953      * Contains class's {@code Token}.
954      *
955      * @param name {@code FullIdent} associated with this class.
956      */
957     private record ClassInfo(Token name) {
958     }
959 
960     /**
961      * Represents text element with location in the text.
962      *
963      * @param text Token's text.
964      */
965     private record Token(String text) {
966 
967         /**
968          * Converts FullIdent to Token.
969          *
970          * @param fullIdent full ident to convert.
971          */
972         private Token(FullIdent fullIdent) {
973             this(fullIdent.getText());
974         }
975     }
976 
977     /** Stores useful information about declared exception. */
978     private static final class ExceptionInfo {
979 
980         /** AST node representing this exception. */
981         private final DetailAST ast;
982 
983         /** Class information associated with this exception. */
984         private final ClassInfo classInfo;
985         /** Does the exception have throws tag associated with. */
986         private boolean found;
987 
988         /**
989          * Creates new instance for {@code FullIdent}.
990          *
991          * @param ast AST node representing this exception
992          * @param classInfo class info
993          */
994         private ExceptionInfo(DetailAST ast, ClassInfo classInfo) {
995             this.ast = ast;
996             this.classInfo = classInfo;
997         }
998 
999         /**
1000          * Gets the AST node representing this exception.
1001          *
1002          * @return the AST node representing this exception
1003          */
1004         private DetailAST getAst() {
1005             return ast;
1006         }
1007 
1008         /** Mark that the exception has associated throws tag. */
1009         private void setFound() {
1010             found = true;
1011         }
1012 
1013         /**
1014          * Checks that the exception has throws tag associated with it.
1015          *
1016          * @return whether the exception has throws tag associated with
1017          */
1018         private boolean isFound() {
1019             return found;
1020         }
1021 
1022         /**
1023          * Gets exception name.
1024          *
1025          * @return exception's name
1026          */
1027         private Token getName() {
1028             return classInfo.name();
1029         }
1030 
1031     }
1032 
1033 }