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