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