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      * 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         for (JavadocTag tag : tags) {
714             if (!tag.isParamTag()) {
715                 continue;
716             }
717 
718             if (isDuplicateParamTag(tags, tag)) {
719                 log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG,
720                         JavadocTagInfo.PARAM.getText());
721             }
722             else {
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         tags.removeIf(JavadocTag::isParamTag);
733 
734         if (!allowMissingParamTags && reportExpectedTags) {
735             for (DetailAST param : params) {
736                 log(compactDef, MSG_EXPECTED_TAG,
737                     JavadocTagInfo.PARAM.getText(), param.getText());
738             }
739         }
740     }
741 
742     /**
743      * Checks a set of tags for matching parameters.
744      *
745      * @param tags the tags to check
746      * @param parent the node which takes the parameters
747      * @param reportExpectedTags whether we should report if do not find
748      *            expected tag
749      */
750     private void checkParamTags(final List<JavadocTag> tags,
751             final DetailAST parent, boolean reportExpectedTags) {
752         final List<DetailAST> params = getParameters(parent);
753         final List<DetailAST> typeParams = CheckUtil
754                 .getTypeParameters(parent);
755 
756         // Loop over the tags, checking to see they exist in the params.
757         for (JavadocTag tag : tags) {
758             if (!tag.isParamTag()) {
759                 continue;
760             }
761 
762             if (isDuplicateParamTag(tags, tag)) {
763                 log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG,
764                         JavadocTagInfo.PARAM.getText());
765             }
766             else {
767                 final String arg1 = tag.getFirstArg();
768                 boolean found = removeMatchingParam(params, arg1);
769 
770                 if (arg1.endsWith(ELEMENT_END)) {
771                     found = searchMatchingTypeParameter(typeParams,
772                             arg1.substring(1, arg1.length() - 1));
773                 }
774 
775                 // Handle extra JavadocTag
776                 if (!found) {
777                     log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
778                             JavadocTagInfo.PARAM.getText(), arg1);
779                 }
780             }
781         }
782         tags.removeIf(JavadocTag::isParamTag);
783 
784         // Now dump out all type parameters/parameters without tags :- unless
785         // the user has chosen to suppress these problems
786         if (!allowMissingParamTags && reportExpectedTags) {
787             for (DetailAST param : params) {
788                 log(param, MSG_EXPECTED_TAG,
789                     JavadocTagInfo.PARAM.getText(), param.getText());
790             }
791 
792             for (DetailAST typeParam : typeParams) {
793                 log(typeParam, MSG_EXPECTED_TAG,
794                     JavadocTagInfo.PARAM.getText(),
795                     ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText()
796                     + ELEMENT_END);
797             }
798         }
799     }
800 
801     /**
802      * Checks if the {@code @param} tag duplicates an earlier {@code @param} tag.
803      *
804      * @param tags all tags to check
805      * @param tag the tag to check
806      * @return true if there is an earlier {@code @param} tag with the same argument
807      */
808     private static boolean isDuplicateParamTag(List<JavadocTag> tags, JavadocTag tag) {
809         final int tagIndex = tags.indexOf(tag);
810         boolean result = false;
811         for (int tagPosition = 0; tagPosition < tagIndex; tagPosition++) {
812             final JavadocTag currentTag = tags.get(tagPosition);
813             if (currentTag.isParamTag()
814                     && currentTag.getFirstArg().equals(tag.getFirstArg())) {
815                 result = true;
816             }
817         }
818         return result;
819     }
820 
821     /**
822      * Returns true if required type found in type parameters.
823      *
824      * @param typeParams
825      *            collection of type parameters
826      * @param requiredTypeName
827      *            name of required type
828      * @return true if required type found in type parameters.
829      */
830     private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams,
831             String requiredTypeName) {
832         // Loop looking for matching type param
833         final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
834         boolean found = false;
835         while (typeParamsIt.hasNext()) {
836             final DetailAST typeParam = typeParamsIt.next();
837             if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
838                     .equals(requiredTypeName)) {
839                 found = true;
840                 typeParamsIt.remove();
841                 break;
842             }
843         }
844         return found;
845     }
846 
847     /**
848      * Remove parameter from params collection by name.
849      *
850      * @param params collection of DetailAST parameters
851      * @param paramName name of parameter
852      * @return true if parameter found and removed
853      */
854     private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) {
855         boolean found = false;
856         final Iterator<DetailAST> paramIt = params.iterator();
857         while (paramIt.hasNext()) {
858             final DetailAST param = paramIt.next();
859             if (param.getText().equals(paramName)) {
860                 found = true;
861                 paramIt.remove();
862                 break;
863             }
864         }
865         return found;
866     }
867 
868     /**
869      * Checks for only one return tag. All return tags will be removed from the
870      * supplied list.
871      *
872      * @param tags the tags to check
873      * @param lineNo the line number of the expected tag
874      * @param reportExpectedTags whether we should report if do not find
875      *            expected tag
876      */
877     private void checkReturnTag(List<JavadocTag> tags, int lineNo,
878         boolean reportExpectedTags) {
879         // Loop over tags finding return tags. After the first one, report a violation
880         boolean found = false;
881         final ListIterator<JavadocTag> it = tags.listIterator();
882         while (it.hasNext()) {
883             final JavadocTag javadocTag = it.next();
884             if (javadocTag.isReturnTag()) {
885                 if (found) {
886                     log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
887                             MSG_DUPLICATE_TAG,
888                             JavadocTagInfo.RETURN.getText());
889                 }
890                 found = true;
891                 it.remove();
892             }
893         }
894 
895         // Handle there being no @return tags :- unless
896         // the user has chosen to suppress these problems
897         if (!found && !allowMissingReturnTag && reportExpectedTags) {
898             log(lineNo, MSG_RETURN_EXPECTED);
899         }
900     }
901 
902     /**
903      * Checks a set of tags for matching throws.
904      *
905      * @param tags the tags to check
906      * @param throwsList the throws to check
907      * @param reportExpectedTags whether we should report if do not find
908      *            expected tag
909      */
910     private void checkThrowsTags(List<JavadocTag> tags,
911             List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
912         // Loop over the tags, checking to see they exist in the throws.
913         final ListIterator<JavadocTag> tagIt = tags.listIterator();
914         while (tagIt.hasNext()) {
915             final JavadocTag tag = tagIt.next();
916 
917             if (!tag.isThrowsTag()) {
918                 continue;
919             }
920             tagIt.remove();
921 
922             // Loop looking for matching throw
923             processThrows(throwsList, tag.getFirstArg());
924         }
925         // Now dump out all throws without tags :- unless
926         // the user has chosen to suppress these problems
927         if (validateThrows && reportExpectedTags) {
928             throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
929                 .forEach(exceptionInfo -> {
930                     final Token token = exceptionInfo.getName();
931                     log(exceptionInfo.getAst(),
932                         MSG_EXPECTED_TAG,
933                         JavadocTagInfo.THROWS.getText(), token.text());
934                 });
935         }
936     }
937 
938     /**
939      * Verifies that documented exception is in throws.
940      *
941      * @param throwsIterable collection of throws
942      * @param documentedClassName documented exception class name
943      */
944     private static void processThrows(Iterable<ExceptionInfo> throwsIterable,
945                                       String documentedClassName) {
946         for (ExceptionInfo exceptionInfo : throwsIterable) {
947             if (isClassNamesSame(exceptionInfo.getName().text(),
948                     documentedClassName)) {
949                 exceptionInfo.setFound();
950                 break;
951             }
952         }
953     }
954 
955     /**
956      * Check that ExceptionInfo objects are same by name.
957      *
958      * @param info1 ExceptionInfo object
959      * @param info2 ExceptionInfo object
960      * @return true is ExceptionInfo object have the same name
961      */
962     private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) {
963         return isClassNamesSame(info1.getName().text(),
964                                     info2.getName().text());
965     }
966 
967     /**
968      * Check that class names are same by short name of class. If some class name is fully
969      * qualified it is cut to short name.
970      *
971      * @param class1 class name
972      * @param class2 class name
973      * @return true is ExceptionInfo object have the same name
974      */
975     private static boolean isClassNamesSame(String class1, String class2) {
976         final String class1ShortName = class1
977                 .substring(class1.lastIndexOf('.') + 1);
978         final String class2ShortName = class2
979                 .substring(class2.lastIndexOf('.') + 1);
980         return class1ShortName.equals(class2ShortName);
981     }
982 
983     /**
984      * Contains class's {@code Token}.
985      *
986      * @param name {@code FullIdent} associated with this class.
987      */
988     private record ClassInfo(Token name) {
989     }
990 
991     /**
992      * Represents text element with location in the text.
993      *
994      * @param text Token's text.
995      */
996     private record Token(String text) {
997 
998         /**
999          * Converts FullIdent to Token.
1000          *
1001          * @param fullIdent full ident to convert.
1002          */
1003         private Token(FullIdent fullIdent) {
1004             this(fullIdent.getText());
1005         }
1006     }
1007 
1008     /** Stores useful information about declared exception. */
1009     private static final class ExceptionInfo {
1010 
1011         /** AST node representing this exception. */
1012         private final DetailAST ast;
1013 
1014         /** Class information associated with this exception. */
1015         private final ClassInfo classInfo;
1016         /** Does the exception have throws tag associated with. */
1017         private boolean found;
1018 
1019         /**
1020          * Creates new instance for {@code FullIdent}.
1021          *
1022          * @param ast AST node representing this exception
1023          * @param classInfo class info
1024          */
1025         private ExceptionInfo(DetailAST ast, ClassInfo classInfo) {
1026             this.ast = ast;
1027             this.classInfo = classInfo;
1028         }
1029 
1030         /**
1031          * Gets the AST node representing this exception.
1032          *
1033          * @return the AST node representing this exception
1034          */
1035         private DetailAST getAst() {
1036             return ast;
1037         }
1038 
1039         /** Mark that the exception has associated throws tag. */
1040         private void setFound() {
1041             found = true;
1042         }
1043 
1044         /**
1045          * Checks that the exception has throws tag associated with it.
1046          *
1047          * @return whether the exception has throws tag associated with
1048          */
1049         private boolean isFound() {
1050             return found;
1051         }
1052 
1053         /**
1054          * Gets exception name.
1055          *
1056          * @return exception's name
1057          */
1058         private Token getName() {
1059             return classInfo.name();
1060         }
1061 
1062     }
1063 
1064 }