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