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