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