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