001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.ListIterator;
029import java.util.Set;
030import java.util.regex.MatchResult;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
035import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
036import com.puppycrawl.tools.checkstyle.api.DetailAST;
037import com.puppycrawl.tools.checkstyle.api.FileContents;
038import com.puppycrawl.tools.checkstyle.api.FullIdent;
039import com.puppycrawl.tools.checkstyle.api.TextBlock;
040import com.puppycrawl.tools.checkstyle.api.TokenTypes;
041import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
042import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
043import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
044import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
045
046/**
047 * <p>
048 * Checks the Javadoc of a method or constructor.
049 * </p>
050 * <p>
051 * Violates parameters and type parameters for which no param tags are present can
052 * be suppressed by defining property {@code allowMissingParamTags}.
053 * </p>
054 * <p>
055 * Violates methods which return non-void but for which no return tag is present can
056 * be suppressed by defining property {@code allowMissingReturnTag}.
057 * </p>
058 * <p>
059 * Violates exceptions which are declared to be thrown (by {@code throws} in the method
060 * signature or by {@code throw new} in the method body), but for which no throws tag is
061 * present by activation of property {@code validateThrows}.
062 * Note that {@code throw new} is not checked in the following places:
063 * </p>
064 * <ul>
065 * <li>
066 * Inside a try block (with catch). It is not possible to determine if the thrown
067 * exception can be caught by the catch block as there is no knowledge of the
068 * inheritance hierarchy, so the try block is ignored entirely. However, catch
069 * and finally blocks, as well as try blocks without catch, are still checked.
070 * </li>
071 * <li>
072 * Local classes, anonymous classes and lambda expressions. It is not known when the
073 * throw statements inside such classes are going to be evaluated, so they are ignored.
074 * </li>
075 * </ul>
076 * <p>
077 * ATTENTION: Checkstyle does not have information about hierarchy of exception types
078 * so usage of base class is considered as separate exception type.
079 * As workaround, you need to specify both types in javadoc (parent and exact type).
080 * </p>
081 * <p>
082 * Javadoc is not required on a method that is tagged with the {@code @Override}
083 * annotation. However, under Java 5 it is not possible to mark a method required
084 * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle
085 * supports using the convention of using a single {@code {@inheritDoc}} tag
086 * instead of all the other tags.
087 * </p>
088 * <p>
089 * Note that only inheritable items will allow the {@code {@inheritDoc}}
090 * tag to be used in place of comments. Static methods at all visibilities,
091 * private non-static methods and constructors are not inheritable.
092 * </p>
093 * <p>
094 * For example, if the following method is implementing a method required by
095 * an interface, then the Javadoc could be done as:
096 * </p>
097 * <pre>
098 * &#47;** {&#64;inheritDoc} *&#47;
099 * public int checkReturnTag(final int aTagIndex,
100 *                           JavadocTag[] aTags,
101 *                           int aLineNo)
102 * </pre>
103 * <ul>
104 * <li>
105 * Property {@code allowedAnnotations} - Specify annotations that allow missed documentation.
106 * Type is {@code java.lang.String[]}.
107 * Default value is {@code Override}.
108 * </li>
109 * <li>
110 * Property {@code validateThrows} - Control whether to validate {@code throws} tags.
111 * Type is {@code boolean}.
112 * Default value is {@code false}.
113 * </li>
114 * <li>
115 * Property {@code accessModifiers} - Specify the access modifiers where Javadoc comments are
116 * checked.
117 * Type is {@code com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption[]}.
118 * Default value is {@code public, protected, package, private}.
119 * </li>
120 * <li>
121 * Property {@code allowMissingParamTags} - Control whether to ignore violations
122 * when a method has parameters but does not have matching {@code param} tags in the javadoc.
123 * Type is {@code boolean}.
124 * Default value is {@code false}.
125 * </li>
126 * <li>
127 * Property {@code allowMissingReturnTag} - Control whether to ignore violations
128 * when a method returns non-void type and does not have a {@code return} tag in the javadoc.
129 * Type is {@code boolean}.
130 * Default value is {@code false}.
131 * </li>
132 * <li>
133 * Property {@code tokens} - tokens to check
134 * Type is {@code java.lang.String[]}.
135 * Validation type is {@code tokenSet}.
136 * Default value is:
137 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
138 * METHOD_DEF</a>,
139 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
140 * CTOR_DEF</a>,
141 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
142 * ANNOTATION_FIELD_DEF</a>,
143 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
144 * COMPACT_CTOR_DEF</a>.
145 * </li>
146 * </ul>
147 * <p>
148 * To configure the default check:
149 * </p>
150 * <pre>
151 * &lt;module name="JavadocMethod"/&gt;
152 * </pre>
153 * <p>
154 * To configure the check for only {@code public} modifier, ignoring any missing param tags is:
155 * </p>
156 * <pre>
157 * &lt;module name="JavadocMethod"&gt;
158 *   &lt;property name="accessModifiers" value="public"/&gt;
159 *   &lt;property name="allowMissingParamTags" value="true"/&gt;
160 * &lt;/module&gt;
161 * </pre>
162 * <p>
163 * To configure the check for methods which are in {@code private} and {@code package},
164 * but not any other modifier:
165 * </p>
166 * <pre>
167 * &lt;module name="JavadocMethod"&gt;
168 *   &lt;property name="accessModifiers" value="private, package"/&gt;
169 * &lt;/module&gt;
170 * </pre>
171 * <p>
172 * To configure the check to validate {@code throws} tags, you can use following config.
173 * </p>
174 * <pre>
175 * &lt;module name="JavadocMethod"&gt;
176 *   &lt;property name="validateThrows" value="true"/&gt;
177 * &lt;/module&gt;
178 * </pre>
179 * <pre>
180 * &#47;**
181 *  * Actual exception thrown is child class of class that is declared in throws.
182 *  * It is limitation of checkstyle (as checkstyle does not know type hierarchy).
183 *  * Javadoc is valid not declaring FileNotFoundException
184 *  * BUT checkstyle can not distinguish relationship between exceptions.
185 *  * &#64;param file some file
186 *  * &#64;throws IOException if some problem
187 *  *&#47;
188 * public void doSomething8(File file) throws IOException {
189 *     if (file == null) {
190 *         throw new FileNotFoundException(); // violation
191 *     }
192 * }
193 *
194 * &#47;**
195 *  * Exact throw type referencing in javadoc even first is parent of second type.
196 *  * It is a limitation of checkstyle (as checkstyle does not know type hierarchy).
197 *  * This javadoc is valid for checkstyle and for javadoc tool.
198 *  * &#64;param file some file
199 *  * &#64;throws IOException if some problem
200 *  * &#64;throws FileNotFoundException if file is not found
201 *  *&#47;
202 * public void doSomething9(File file) throws IOException {
203 *     if (file == null) {
204 *         throw new FileNotFoundException();
205 *     }
206 * }
207 *
208 * &#47;**
209 *  * Ignore try block, but keep catch and finally blocks.
210 *  *
211 *  * &#64;param s String to parse
212 *  * &#64;return A positive integer
213 *  *&#47;
214 * public int parsePositiveInt(String s) {
215 *     try {
216 *         int value = Integer.parseInt(s);
217 *         if (value &lt;= 0) {
218 *             throw new NumberFormatException(value + " is negative/zero"); // ok, try
219 *         }
220 *         return value;
221 *     } catch (NumberFormatException ex) {
222 *         throw new IllegalArgumentException("Invalid number", ex); // violation, catch
223 *     } finally {
224 *         throw new IllegalStateException("Should never reach here"); // violation, finally
225 *     }
226 * }
227 *
228 * &#47;**
229 *  * Try block without catch is not ignored.
230 *  *
231 *  * &#64;return a String from standard input, if there is one
232 *  *&#47;
233 * public String readLine() {
234 *     try (Scanner sc = new Scanner(System.in)) {
235 *         if (!sc.hasNext()) {
236 *             throw new IllegalStateException("Empty input"); // violation, not caught
237 *         }
238 *         return sc.next();
239 *     }
240 * }
241 *
242 * &#47;**
243 *  * Lambda expressions are ignored as we do not know when the exception will be thrown.
244 *  *
245 *  * &#64;param s a String to be printed at some point in the future
246 *  * &#64;return a Runnable to be executed when the string is to be printed
247 *  *&#47;
248 * public Runnable printLater(String s) {
249 *     return () -&gt; {
250 *         if (s == null) {
251 *             throw new NullPointerException(); // ok
252 *         }
253 *         System.out.println(s);
254 *     };
255 * }
256 * </pre>
257 * <p>
258 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
259 * </p>
260 * <p>
261 * Violation Message Keys:
262 * </p>
263 * <ul>
264 * <li>
265 * {@code javadoc.classInfo}
266 * </li>
267 * <li>
268 * {@code javadoc.duplicateTag}
269 * </li>
270 * <li>
271 * {@code javadoc.expectedTag}
272 * </li>
273 * <li>
274 * {@code javadoc.invalidInheritDoc}
275 * </li>
276 * <li>
277 * {@code javadoc.return.expected}
278 * </li>
279 * <li>
280 * {@code javadoc.unusedTag}
281 * </li>
282 * <li>
283 * {@code javadoc.unusedTagGeneral}
284 * </li>
285 * </ul>
286 *
287 * @since 3.0
288 */
289@FileStatefulCheck
290public class JavadocMethodCheck extends AbstractCheck {
291
292    /**
293     * A key is pointing to the warning message text in "messages.properties"
294     * file.
295     */
296    public static final String MSG_CLASS_INFO = "javadoc.classInfo";
297
298    /**
299     * A key is pointing to the warning message text in "messages.properties"
300     * file.
301     */
302    public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
303
304    /**
305     * A key is pointing to the warning message text in "messages.properties"
306     * file.
307     */
308    public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc";
309
310    /**
311     * A key is pointing to the warning message text in "messages.properties"
312     * file.
313     */
314    public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
315
316    /**
317     * A key is pointing to the warning message text in "messages.properties"
318     * file.
319     */
320    public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag";
321
322    /**
323     * A key is pointing to the warning message text in "messages.properties"
324     * file.
325     */
326    public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected";
327
328    /**
329     * A key is pointing to the warning message text in "messages.properties"
330     * file.
331     */
332    public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag";
333
334    /** Compiled regexp to match Javadoc tags that take an argument. */
335    private static final Pattern MATCH_JAVADOC_ARG = CommonUtil.createPattern(
336            "^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+(\\S+)\\s+\\S*");
337    /** Compiled regexp to match Javadoc tags with argument but with missing description. */
338    private static final Pattern MATCH_JAVADOC_ARG_MISSING_DESCRIPTION =
339        CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+"
340            + "(\\S[^*]*)(?:(\\s+|\\*\\/))?");
341
342    /** Compiled regexp to look for a continuation of the comment. */
343    private static final Pattern MATCH_JAVADOC_MULTILINE_CONT =
344            CommonUtil.createPattern("(\\*\\/|@|[^\\s\\*])");
345
346    /** Multiline finished at end of comment. */
347    private static final String END_JAVADOC = "*/";
348    /** Multiline finished at next Javadoc. */
349    private static final String NEXT_TAG = "@";
350
351    /** Compiled regexp to match Javadoc tags with no argument. */
352    private static final Pattern MATCH_JAVADOC_NOARG =
353            CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s+\\S");
354    /** Compiled regexp to match first part of multilineJavadoc tags. */
355    private static final Pattern MATCH_JAVADOC_NOARG_MULTILINE_START =
356            CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s*$");
357    /** Compiled regexp to match Javadoc tags with no argument and {}. */
358    private static final Pattern MATCH_JAVADOC_NOARG_CURLY =
359            CommonUtil.createPattern("\\{\\s*@(inheritDoc)\\s*\\}");
360
361    /** Name of current class. */
362    private String currentClassName;
363
364    /** Specify the access modifiers where Javadoc comments are checked. */
365    private AccessModifierOption[] accessModifiers = {
366        AccessModifierOption.PUBLIC,
367        AccessModifierOption.PROTECTED,
368        AccessModifierOption.PACKAGE,
369        AccessModifierOption.PRIVATE,
370    };
371
372    /**
373     * Control whether to validate {@code throws} tags.
374     */
375    private boolean validateThrows;
376
377    /**
378     * Control whether to ignore violations when a method has parameters but does
379     * not have matching {@code param} tags in the javadoc.
380     */
381    private boolean allowMissingParamTags;
382
383    /**
384     * Control whether to ignore violations when a method returns non-void type
385     * and does not have a {@code return} tag in the javadoc.
386     */
387    private boolean allowMissingReturnTag;
388
389    /** Specify annotations that allow missed documentation. */
390    private Set<String> allowedAnnotations = Set.of("Override");
391
392    /**
393     * Setter to control whether to validate {@code throws} tags.
394     *
395     * @param value user's value.
396     */
397    public void setValidateThrows(boolean value) {
398        validateThrows = value;
399    }
400
401    /**
402     * Setter to specify annotations that allow missed documentation.
403     *
404     * @param userAnnotations user's value.
405     */
406    public void setAllowedAnnotations(String... userAnnotations) {
407        allowedAnnotations = Set.of(userAnnotations);
408    }
409
410    /**
411     * Setter to specify the access modifiers where Javadoc comments are checked.
412     *
413     * @param accessModifiers access modifiers.
414     */
415    public void setAccessModifiers(AccessModifierOption... accessModifiers) {
416        this.accessModifiers =
417            Arrays.copyOf(accessModifiers, accessModifiers.length);
418    }
419
420    /**
421     * Setter to control whether to ignore violations when a method has parameters
422     * but does not have matching {@code param} tags in the javadoc.
423     *
424     * @param flag a {@code Boolean} value
425     */
426    public void setAllowMissingParamTags(boolean flag) {
427        allowMissingParamTags = flag;
428    }
429
430    /**
431     * Setter to control whether to ignore violations when a method returns non-void type
432     * and does not have a {@code return} tag in the javadoc.
433     *
434     * @param flag a {@code Boolean} value
435     */
436    public void setAllowMissingReturnTag(boolean flag) {
437        allowMissingReturnTag = flag;
438    }
439
440    @Override
441    public final int[] getRequiredTokens() {
442        return new int[] {
443            TokenTypes.CLASS_DEF,
444            TokenTypes.INTERFACE_DEF,
445            TokenTypes.ENUM_DEF,
446            TokenTypes.RECORD_DEF,
447        };
448    }
449
450    @Override
451    public int[] getDefaultTokens() {
452        return getAcceptableTokens();
453    }
454
455    @Override
456    public int[] getAcceptableTokens() {
457        return new int[] {
458            TokenTypes.CLASS_DEF,
459            TokenTypes.ENUM_DEF,
460            TokenTypes.INTERFACE_DEF,
461            TokenTypes.METHOD_DEF,
462            TokenTypes.CTOR_DEF,
463            TokenTypes.ANNOTATION_FIELD_DEF,
464            TokenTypes.RECORD_DEF,
465            TokenTypes.COMPACT_CTOR_DEF,
466        };
467    }
468
469    @Override
470    public void beginTree(DetailAST rootAST) {
471        currentClassName = "";
472    }
473
474    @Override
475    public final void visitToken(DetailAST ast) {
476        if (ast.getType() == TokenTypes.CLASS_DEF
477                 || ast.getType() == TokenTypes.INTERFACE_DEF
478                 || ast.getType() == TokenTypes.ENUM_DEF
479                 || ast.getType() == TokenTypes.RECORD_DEF) {
480            processClass(ast);
481        }
482        else {
483            processAST(ast);
484        }
485    }
486
487    @Override
488    public final void leaveToken(DetailAST ast) {
489        if (ast.getType() == TokenTypes.CLASS_DEF
490            || ast.getType() == TokenTypes.INTERFACE_DEF
491            || ast.getType() == TokenTypes.ENUM_DEF
492            || ast.getType() == TokenTypes.RECORD_DEF) {
493            // perhaps it was inner class
494            final int dotIdx = currentClassName.lastIndexOf('$');
495            currentClassName = currentClassName.substring(0, dotIdx);
496        }
497    }
498
499    /**
500     * Called to process an AST when visiting it.
501     *
502     * @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or
503     *             IMPORT tokens.
504     */
505    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
506    @SuppressWarnings("deprecation")
507    private void processAST(DetailAST ast) {
508        if (shouldCheck(ast)) {
509            final FileContents contents = getFileContents();
510            final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo());
511
512            if (textBlock != null) {
513                checkComment(ast, textBlock);
514            }
515        }
516    }
517
518    /**
519     * Whether we should check this node.
520     *
521     * @param ast a given node.
522     * @return whether we should check a given node.
523     */
524    private boolean shouldCheck(final DetailAST ast) {
525        final AccessModifierOption surroundingAccessModifier = CheckUtil
526                .getSurroundingAccessModifier(ast);
527        final AccessModifierOption accessModifier = CheckUtil
528                .getAccessModifierFromModifiersToken(ast);
529        return surroundingAccessModifier != null
530                && Arrays.stream(accessModifiers)
531                        .anyMatch(modifier -> modifier == surroundingAccessModifier)
532                && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier);
533    }
534
535    /**
536     * Checks the Javadoc for a method.
537     *
538     * @param ast the token for the method
539     * @param comment the Javadoc comment
540     */
541    private void checkComment(DetailAST ast, TextBlock comment) {
542        final List<JavadocTag> tags = getMethodTags(comment);
543
544        if (!hasShortCircuitTag(ast, tags)) {
545            if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) {
546                checkReturnTag(tags, ast.getLineNo(), true);
547            }
548            else {
549                final Iterator<JavadocTag> it = tags.iterator();
550                // Check for inheritDoc
551                boolean hasInheritDocTag = false;
552                while (!hasInheritDocTag && it.hasNext()) {
553                    hasInheritDocTag = it.next().isInheritDocTag();
554                }
555                final boolean reportExpectedTags = !hasInheritDocTag
556                    && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
557
558                // COMPACT_CTOR_DEF has no parameters
559                if (ast.getType() != TokenTypes.COMPACT_CTOR_DEF) {
560                    checkParamTags(tags, ast, reportExpectedTags);
561                }
562                final List<ExceptionInfo> throwed =
563                    combineExceptionInfo(getThrows(ast), getThrowed(ast));
564                checkThrowsTags(tags, throwed, reportExpectedTags);
565                if (CheckUtil.isNonVoidMethod(ast)) {
566                    checkReturnTag(tags, ast.getLineNo(), reportExpectedTags);
567                }
568
569            }
570
571            // Dump out all unused tags
572            tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag())
573                .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL));
574        }
575    }
576
577    /**
578     * Validates whether the Javadoc has a short circuit tag. Currently, this is
579     * the inheritTag. Any violations are logged.
580     *
581     * @param ast the construct being checked
582     * @param tags the list of Javadoc tags associated with the construct
583     * @return true if the construct has a short circuit tag.
584     */
585    private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) {
586        boolean result = true;
587        // Check if it contains {@inheritDoc} tag
588        if (tags.size() == 1
589                && tags.get(0).isInheritDocTag()) {
590            // Invalid if private, a constructor, or a static method
591            if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) {
592                log(ast, MSG_INVALID_INHERIT_DOC);
593            }
594        }
595        else {
596            result = false;
597        }
598        return result;
599    }
600
601    /**
602     * Returns the tags in a javadoc comment. Only finds throws, exception,
603     * param, return and see tags.
604     *
605     * @param comment the Javadoc comment
606     * @return the tags found
607     */
608    private static List<JavadocTag> getMethodTags(TextBlock comment) {
609        final String[] lines = comment.getText();
610        final List<JavadocTag> tags = new ArrayList<>();
611        int currentLine = comment.getStartLineNo() - 1;
612        final int startColumnNumber = comment.getStartColNo();
613
614        for (int i = 0; i < lines.length; i++) {
615            currentLine++;
616            final Matcher javadocArgMatcher =
617                MATCH_JAVADOC_ARG.matcher(lines[i]);
618            final Matcher javadocArgMissingDescriptionMatcher =
619                MATCH_JAVADOC_ARG_MISSING_DESCRIPTION.matcher(lines[i]);
620            final Matcher javadocNoargMatcher =
621                MATCH_JAVADOC_NOARG.matcher(lines[i]);
622            final Matcher noargCurlyMatcher =
623                MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]);
624            final Matcher noargMultilineStart =
625                MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]);
626
627            if (javadocArgMatcher.find()) {
628                final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber);
629                tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1),
630                        javadocArgMatcher.group(2)));
631            }
632            else if (javadocArgMissingDescriptionMatcher.find()) {
633                final int col = calculateTagColumn(javadocArgMissingDescriptionMatcher, i,
634                    startColumnNumber);
635                tags.add(new JavadocTag(currentLine, col,
636                    javadocArgMissingDescriptionMatcher.group(1),
637                    javadocArgMissingDescriptionMatcher.group(2)));
638            }
639            else if (javadocNoargMatcher.find()) {
640                final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber);
641                tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1)));
642            }
643            else if (noargCurlyMatcher.find()) {
644                final int col = calculateTagColumn(noargCurlyMatcher, i, startColumnNumber);
645                tags.add(new JavadocTag(currentLine, col, noargCurlyMatcher.group(1)));
646            }
647            else if (noargMultilineStart.find()) {
648                tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine));
649            }
650        }
651        return tags;
652    }
653
654    /**
655     * Calculates column number using Javadoc tag matcher.
656     *
657     * @param javadocTagMatchResult found javadoc tag match result
658     * @param lineNumber line number of Javadoc tag in comment
659     * @param startColumnNumber column number of Javadoc comment beginning
660     * @return column number
661     */
662    private static int calculateTagColumn(MatchResult javadocTagMatchResult,
663            int lineNumber, int startColumnNumber) {
664        int col = javadocTagMatchResult.start(1) - 1;
665        if (lineNumber == 0) {
666            col += startColumnNumber;
667        }
668        return col;
669    }
670
671    /**
672     * Gets multiline Javadoc tags with no arguments.
673     *
674     * @param noargMultilineStart javadoc tag Matcher
675     * @param lines comment text lines
676     * @param lineIndex line number that contains the javadoc tag
677     * @param tagLine javadoc tag line number in file
678     * @return javadoc tags with no arguments
679     */
680    private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart,
681            final String[] lines, final int lineIndex, final int tagLine) {
682        int remIndex = lineIndex;
683        Matcher multilineCont;
684
685        do {
686            remIndex++;
687            multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]);
688        } while (!multilineCont.find());
689
690        final List<JavadocTag> tags = new ArrayList<>();
691        final String lFin = multilineCont.group(1);
692        if (!NEXT_TAG.equals(lFin)
693            && !END_JAVADOC.equals(lFin)) {
694            final String param1 = noargMultilineStart.group(1);
695            final int col = noargMultilineStart.start(1) - 1;
696
697            tags.add(new JavadocTag(tagLine, col, param1));
698        }
699
700        return tags;
701    }
702
703    /**
704     * Computes the parameter nodes for a method.
705     *
706     * @param ast the method node.
707     * @return the list of parameter nodes for ast.
708     */
709    private static List<DetailAST> getParameters(DetailAST ast) {
710        final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
711        final List<DetailAST> returnValue = new ArrayList<>();
712
713        DetailAST child = params.getFirstChild();
714        while (child != null) {
715            if (child.getType() == TokenTypes.PARAMETER_DEF) {
716                final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
717                if (ident != null) {
718                    returnValue.add(ident);
719                }
720            }
721            child = child.getNextSibling();
722        }
723        return returnValue;
724    }
725
726    /**
727     * Computes the exception nodes for a method.
728     *
729     * @param ast the method node.
730     * @return the list of exception nodes for ast.
731     */
732    private static List<ExceptionInfo> getThrows(DetailAST ast) {
733        final List<ExceptionInfo> returnValue = new ArrayList<>();
734        final DetailAST throwsAST = ast
735                .findFirstToken(TokenTypes.LITERAL_THROWS);
736        if (throwsAST != null) {
737            DetailAST child = throwsAST.getFirstChild();
738            while (child != null) {
739                if (child.getType() == TokenTypes.IDENT
740                        || child.getType() == TokenTypes.DOT) {
741                    returnValue.add(getExceptionInfo(child));
742                }
743                child = child.getNextSibling();
744            }
745        }
746        return returnValue;
747    }
748
749    /**
750     * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'.
751     *
752     * @param methodAst method DetailAST object where to find exceptions
753     * @return list of ExceptionInfo
754     */
755    private static List<ExceptionInfo> getThrowed(DetailAST methodAst) {
756        final List<ExceptionInfo> returnValue = new ArrayList<>();
757        final DetailAST blockAst = methodAst.findFirstToken(TokenTypes.SLIST);
758        if (blockAst != null) {
759            final List<DetailAST> throwLiterals = findTokensInAstByType(blockAst,
760                    TokenTypes.LITERAL_THROW);
761            for (DetailAST throwAst : throwLiterals) {
762                if (!isInIgnoreBlock(blockAst, throwAst)) {
763                    final DetailAST newAst = throwAst.getFirstChild().getFirstChild();
764                    if (newAst.getType() == TokenTypes.LITERAL_NEW) {
765                        final DetailAST child = newAst.getFirstChild();
766                        returnValue.add(getExceptionInfo(child));
767                    }
768                }
769            }
770        }
771        return returnValue;
772    }
773
774    /**
775     * Get ExceptionInfo instance.
776     *
777     * @param ast DetailAST object where to find exceptions node;
778     * @return ExceptionInfo
779     */
780    private static ExceptionInfo getExceptionInfo(DetailAST ast) {
781        final FullIdent ident = FullIdent.createFullIdent(ast);
782        final DetailAST firstClassNameNode = getFirstClassNameNode(ast);
783        return new ExceptionInfo(firstClassNameNode,
784                new ClassInfo(new Token(ident)));
785    }
786
787    /**
788     * Get node where class name of exception starts.
789     *
790     * @param ast DetailAST object where to find exceptions node;
791     * @return exception node where class name starts
792     */
793    private static DetailAST getFirstClassNameNode(DetailAST ast) {
794        DetailAST startNode = ast;
795        while (startNode.getType() == TokenTypes.DOT) {
796            startNode = startNode.getFirstChild();
797        }
798        return startNode;
799    }
800
801    /**
802     * Checks if a 'throw' usage is contained within a block that should be ignored.
803     * Such blocks consist of try (with catch) blocks, local classes, anonymous classes,
804     * and lambda expressions. Note that a try block without catch is not considered.
805     *
806     * @param methodBodyAst DetailAST node representing the method body
807     * @param throwAst DetailAST node representing the 'throw' literal
808     * @return true if throwAst is inside a block that should be ignored
809     */
810    private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) {
811        DetailAST ancestor = throwAst.getParent();
812        while (ancestor != methodBodyAst) {
813            if (ancestor.getType() == TokenTypes.LITERAL_TRY
814                    && ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null
815                    || ancestor.getType() == TokenTypes.LAMBDA
816                    || ancestor.getType() == TokenTypes.OBJBLOCK) {
817                // throw is inside a try block, and there is a catch block,
818                // or throw is inside a lambda expression/anonymous class/local class
819                break;
820            }
821            if (ancestor.getType() == TokenTypes.LITERAL_CATCH
822                    || ancestor.getType() == TokenTypes.LITERAL_FINALLY) {
823                // if the throw is inside a catch or finally block,
824                // skip the immediate ancestor (try token)
825                ancestor = ancestor.getParent();
826            }
827            ancestor = ancestor.getParent();
828        }
829        return ancestor != methodBodyAst;
830    }
831
832    /**
833     * Combine ExceptionInfo collections together by matching names.
834     *
835     * @param first the first collection of ExceptionInfo
836     * @param second the second collection of ExceptionInfo
837     * @return combined list of ExceptionInfo
838     */
839    private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first,
840                                                            Iterable<ExceptionInfo> second) {
841        final List<ExceptionInfo> result = new ArrayList<>(first);
842        for (ExceptionInfo exceptionInfo : second) {
843            if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) {
844                result.add(exceptionInfo);
845            }
846        }
847        return result;
848    }
849
850    /**
851     * Finds node of specified type among root children, siblings, siblings children
852     * on any deep level.
853     *
854     * @param root    DetailAST
855     * @param astType value of TokenType
856     * @return {@link List} of {@link DetailAST} nodes which matches the predicate.
857     */
858    public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) {
859        final List<DetailAST> result = new ArrayList<>();
860        // iterative preorder depth-first search
861        DetailAST curNode = root;
862        do {
863            // process curNode
864            if (curNode.getType() == astType) {
865                result.add(curNode);
866            }
867            // process children (if any)
868            if (curNode.hasChildren()) {
869                curNode = curNode.getFirstChild();
870                continue;
871            }
872            // backtrack to parent if last child, stopping at root
873            while (curNode != root && curNode.getNextSibling() == null) {
874                curNode = curNode.getParent();
875            }
876            // explore siblings if not root
877            if (curNode != root) {
878                curNode = curNode.getNextSibling();
879            }
880        } while (curNode != root);
881        return result;
882    }
883
884    /**
885     * Checks a set of tags for matching parameters.
886     *
887     * @param tags the tags to check
888     * @param parent the node which takes the parameters
889     * @param reportExpectedTags whether we should report if do not find
890     *            expected tag
891     */
892    private void checkParamTags(final List<JavadocTag> tags,
893            final DetailAST parent, boolean reportExpectedTags) {
894        final List<DetailAST> params = getParameters(parent);
895        final List<DetailAST> typeParams = CheckUtil
896                .getTypeParameters(parent);
897
898        // Loop over the tags, checking to see they exist in the params.
899        final ListIterator<JavadocTag> tagIt = tags.listIterator();
900        while (tagIt.hasNext()) {
901            final JavadocTag tag = tagIt.next();
902
903            if (!tag.isParamTag()) {
904                continue;
905            }
906
907            tagIt.remove();
908
909            final String arg1 = tag.getFirstArg();
910            boolean found = removeMatchingParam(params, arg1);
911
912            if (CommonUtil.startsWithChar(arg1, '<') && CommonUtil.endsWithChar(arg1, '>')) {
913                found = searchMatchingTypeParameter(typeParams,
914                        arg1.substring(1, arg1.length() - 1));
915            }
916
917            // Handle extra JavadocTag
918            if (!found) {
919                log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
920                        "@param", arg1);
921            }
922        }
923
924        // Now dump out all type parameters/parameters without tags :- unless
925        // the user has chosen to suppress these problems
926        if (!allowMissingParamTags && reportExpectedTags) {
927            for (DetailAST param : params) {
928                log(param, MSG_EXPECTED_TAG,
929                    JavadocTagInfo.PARAM.getText(), param.getText());
930            }
931
932            for (DetailAST typeParam : typeParams) {
933                log(typeParam, MSG_EXPECTED_TAG,
934                    JavadocTagInfo.PARAM.getText(),
935                    "<" + typeParam.findFirstToken(TokenTypes.IDENT).getText()
936                    + ">");
937            }
938        }
939    }
940
941    /**
942     * Returns true if required type found in type parameters.
943     *
944     * @param typeParams
945     *            collection of type parameters
946     * @param requiredTypeName
947     *            name of required type
948     * @return true if required type found in type parameters.
949     */
950    private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams,
951            String requiredTypeName) {
952        // Loop looking for matching type param
953        final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
954        boolean found = false;
955        while (typeParamsIt.hasNext()) {
956            final DetailAST typeParam = typeParamsIt.next();
957            if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
958                    .equals(requiredTypeName)) {
959                found = true;
960                typeParamsIt.remove();
961                break;
962            }
963        }
964        return found;
965    }
966
967    /**
968     * Remove parameter from params collection by name.
969     *
970     * @param params collection of DetailAST parameters
971     * @param paramName name of parameter
972     * @return true if parameter found and removed
973     */
974    private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) {
975        boolean found = false;
976        final Iterator<DetailAST> paramIt = params.iterator();
977        while (paramIt.hasNext()) {
978            final DetailAST param = paramIt.next();
979            if (param.getText().equals(paramName)) {
980                found = true;
981                paramIt.remove();
982                break;
983            }
984        }
985        return found;
986    }
987
988    /**
989     * Checks for only one return tag. All return tags will be removed from the
990     * supplied list.
991     *
992     * @param tags the tags to check
993     * @param lineNo the line number of the expected tag
994     * @param reportExpectedTags whether we should report if do not find
995     *            expected tag
996     */
997    private void checkReturnTag(List<JavadocTag> tags, int lineNo,
998        boolean reportExpectedTags) {
999        // Loop over tags finding return tags. After the first one, report a
1000        // violation.
1001        boolean found = false;
1002        final ListIterator<JavadocTag> it = tags.listIterator();
1003        while (it.hasNext()) {
1004            final JavadocTag javadocTag = it.next();
1005            if (javadocTag.isReturnTag()) {
1006                if (found) {
1007                    log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
1008                            MSG_DUPLICATE_TAG,
1009                            JavadocTagInfo.RETURN.getText());
1010                }
1011                found = true;
1012                it.remove();
1013            }
1014        }
1015
1016        // Handle there being no @return tags :- unless
1017        // the user has chosen to suppress these problems
1018        if (!found && !allowMissingReturnTag && reportExpectedTags) {
1019            log(lineNo, MSG_RETURN_EXPECTED);
1020        }
1021    }
1022
1023    /**
1024     * Checks a set of tags for matching throws.
1025     *
1026     * @param tags the tags to check
1027     * @param throwsList the throws to check
1028     * @param reportExpectedTags whether we should report if do not find
1029     *            expected tag
1030     */
1031    private void checkThrowsTags(List<JavadocTag> tags,
1032            List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
1033        // Loop over the tags, checking to see they exist in the throws.
1034        // The foundThrows used for performance only
1035        final Set<String> foundThrows = new HashSet<>();
1036        final ListIterator<JavadocTag> tagIt = tags.listIterator();
1037        while (tagIt.hasNext()) {
1038            final JavadocTag tag = tagIt.next();
1039
1040            if (!tag.isThrowsTag()) {
1041                continue;
1042            }
1043            tagIt.remove();
1044
1045            // Loop looking for matching throw
1046            final Token token = new Token(tag.getFirstArg(), tag.getLineNo(), tag
1047                    .getColumnNo());
1048            final ClassInfo documentedClassInfo = new ClassInfo(token);
1049            processThrows(throwsList, documentedClassInfo, foundThrows);
1050        }
1051        // Now dump out all throws without tags :- unless
1052        // the user has chosen to suppress these problems
1053        if (validateThrows && reportExpectedTags) {
1054            throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
1055                .forEach(exceptionInfo -> {
1056                    final Token token = exceptionInfo.getName();
1057                    log(exceptionInfo.getAst(),
1058                        MSG_EXPECTED_TAG,
1059                        JavadocTagInfo.THROWS.getText(), token.getText());
1060                });
1061        }
1062    }
1063
1064    /**
1065     * Verifies that documented exception is in throws.
1066     *
1067     * @param throwsIterable collection of throws
1068     * @param documentedClassInfo documented exception class info
1069     * @param foundThrows previously found throws
1070     */
1071    private static void processThrows(Iterable<ExceptionInfo> throwsIterable,
1072                                      ClassInfo documentedClassInfo, Set<String> foundThrows) {
1073        ExceptionInfo foundException = null;
1074
1075        // First look for matches on the exception name
1076        for (ExceptionInfo exceptionInfo : throwsIterable) {
1077            if (isClassNamesSame(exceptionInfo.getName().getText(),
1078                    documentedClassInfo.getName().getText())) {
1079                foundException = exceptionInfo;
1080                break;
1081            }
1082        }
1083
1084        if (foundException != null) {
1085            foundException.setFound();
1086            foundThrows.add(documentedClassInfo.getName().getText());
1087        }
1088    }
1089
1090    /**
1091     * Check that ExceptionInfo objects are same by name.
1092     *
1093     * @param info1 ExceptionInfo object
1094     * @param info2 ExceptionInfo object
1095     * @return true is ExceptionInfo object have the same name
1096     */
1097    private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) {
1098        return isClassNamesSame(info1.getName().getText(),
1099                                    info2.getName().getText());
1100    }
1101
1102    /**
1103     * Check that class names are same by short name of class. If some class name is fully
1104     * qualified it is cut to short name.
1105     *
1106     * @param class1 class name
1107     * @param class2 class name
1108     * @return true is ExceptionInfo object have the same name
1109     */
1110    private static boolean isClassNamesSame(String class1, String class2) {
1111        boolean result = false;
1112        if (class1.equals(class2)) {
1113            result = true;
1114        }
1115        else {
1116            final String separator = ".";
1117            if (class1.contains(separator) || class2.contains(separator)) {
1118                final String class1ShortName = class1
1119                        .substring(class1.lastIndexOf('.') + 1);
1120                final String class2ShortName = class2
1121                        .substring(class2.lastIndexOf('.') + 1);
1122                result = class1ShortName.equals(class2ShortName);
1123            }
1124        }
1125        return result;
1126    }
1127
1128    /**
1129     * Processes class definition.
1130     *
1131     * @param ast class definition to process.
1132     */
1133    private void processClass(DetailAST ast) {
1134        final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
1135        String innerClass = ident.getText();
1136
1137        innerClass = "$" + innerClass;
1138        currentClassName += innerClass;
1139    }
1140
1141    /**
1142     * Contains class's {@code Token}.
1143     */
1144    private static class ClassInfo {
1145
1146        /** {@code FullIdent} associated with this class. */
1147        private final Token name;
1148
1149        /**
1150         * Creates new instance of class information object.
1151         *
1152         * @param className token which represents class name.
1153         * @throws IllegalArgumentException when className is nulls
1154         */
1155        protected ClassInfo(final Token className) {
1156            name = className;
1157        }
1158
1159        /**
1160         * Gets class name.
1161         *
1162         * @return class name
1163         */
1164        public final Token getName() {
1165            return name;
1166        }
1167
1168    }
1169
1170    /**
1171     * Represents text element with location in the text.
1172     */
1173    private static final class Token {
1174
1175        /** Token's column number. */
1176        private final int columnNo;
1177        /** Token's line number. */
1178        private final int lineNo;
1179        /** Token's text. */
1180        private final String text;
1181
1182        /**
1183         * Creates token.
1184         *
1185         * @param text token's text
1186         * @param lineNo token's line number
1187         * @param columnNo token's column number
1188         */
1189        private Token(String text, int lineNo, int columnNo) {
1190            this.text = text;
1191            this.lineNo = lineNo;
1192            this.columnNo = columnNo;
1193        }
1194
1195        /**
1196         * Converts FullIdent to Token.
1197         *
1198         * @param fullIdent full ident to convert.
1199         */
1200        private Token(FullIdent fullIdent) {
1201            text = fullIdent.getText();
1202            lineNo = fullIdent.getLineNo();
1203            columnNo = fullIdent.getColumnNo();
1204        }
1205
1206        /**
1207         * Gets text of the token.
1208         *
1209         * @return text of the token
1210         */
1211        public String getText() {
1212            return text;
1213        }
1214
1215        @Override
1216        public String toString() {
1217            return "Token[" + text + "(" + lineNo
1218                + "x" + columnNo + ")]";
1219        }
1220
1221    }
1222
1223    /** Stores useful information about declared exception. */
1224    private static final class ExceptionInfo {
1225
1226        /** AST node representing this exception. */
1227        private final DetailAST ast;
1228
1229        /** Class information associated with this exception. */
1230        private final ClassInfo classInfo;
1231        /** Does the exception have throws tag associated with. */
1232        private boolean found;
1233
1234        /**
1235         * Creates new instance for {@code FullIdent}.
1236         *
1237         * @param ast AST node representing this exception
1238         * @param classInfo class info
1239         */
1240        private ExceptionInfo(DetailAST ast, ClassInfo classInfo) {
1241            this.ast = ast;
1242            this.classInfo = classInfo;
1243        }
1244
1245        /**
1246         * Gets the AST node representing this exception.
1247         *
1248         * @return the AST node representing this exception
1249         */
1250        private DetailAST getAst() {
1251            return ast;
1252        }
1253
1254        /** Mark that the exception has associated throws tag. */
1255        private void setFound() {
1256            found = true;
1257        }
1258
1259        /**
1260         * Checks that the exception has throws tag associated with it.
1261         *
1262         * @return whether the exception has throws tag associated with
1263         */
1264        private boolean isFound() {
1265            return found;
1266        }
1267
1268        /**
1269         * Gets exception name.
1270         *
1271         * @return exception's name
1272         */
1273        private Token getName() {
1274            return classInfo.getName();
1275        }
1276
1277    }
1278
1279}