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