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