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