001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.ArrayDeque;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.Deque;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.List;
031import java.util.ListIterator;
032import java.util.Map;
033import java.util.Set;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
038import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
039import com.puppycrawl.tools.checkstyle.api.DetailAST;
040import com.puppycrawl.tools.checkstyle.api.FileContents;
041import com.puppycrawl.tools.checkstyle.api.FullIdent;
042import com.puppycrawl.tools.checkstyle.api.Scope;
043import com.puppycrawl.tools.checkstyle.api.TextBlock;
044import com.puppycrawl.tools.checkstyle.api.TokenTypes;
045import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
046import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
047import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
048import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
049
050/**
051 * Checks the Javadoc of a method or constructor.
052 */
053@FileStatefulCheck
054public class JavadocMethodCheck extends AbstractCheck {
055
056    /**
057     * A key is pointing to the warning message text in "messages.properties"
058     * file.
059     */
060    public static final String MSG_CLASS_INFO = "javadoc.classInfo";
061
062    /**
063     * A key is pointing to the warning message text in "messages.properties"
064     * file.
065     */
066    public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
067
068    /**
069     * A key is pointing to the warning message text in "messages.properties"
070     * file.
071     */
072    public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc";
073
074    /**
075     * A key is pointing to the warning message text in "messages.properties"
076     * file.
077     */
078    public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag";
085
086    /**
087     * A key is pointing to the warning message text in "messages.properties"
088     * file.
089     */
090    public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected";
091
092    /**
093     * A key is pointing to the warning message text in "messages.properties"
094     * file.
095     */
096    public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag";
097
098    /** Compiled regexp to match Javadoc tags that take an argument. */
099    private static final Pattern MATCH_JAVADOC_ARG = CommonUtil.createPattern(
100            "^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+(\\S+)\\s+\\S*");
101
102    /** Compiled regexp to match first part of multilineJavadoc tags. */
103    private static final Pattern MATCH_JAVADOC_ARG_MULTILINE_START = CommonUtil.createPattern(
104            "^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+(\\S+)\\s*$");
105
106    /** Compiled regexp to look for a continuation of the comment. */
107    private static final Pattern MATCH_JAVADOC_MULTILINE_CONT =
108            CommonUtil.createPattern("(\\*/|@|[^\\s\\*])");
109
110    /** Multiline finished at end of comment. */
111    private static final String END_JAVADOC = "*/";
112    /** Multiline finished at next Javadoc. */
113    private static final String NEXT_TAG = "@";
114
115    /** Compiled regexp to match Javadoc tags with no argument. */
116    private static final Pattern MATCH_JAVADOC_NOARG =
117            CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s+\\S");
118    /** Compiled regexp to match first part of multilineJavadoc tags. */
119    private static final Pattern MATCH_JAVADOC_NOARG_MULTILINE_START =
120            CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s*$");
121    /** Compiled regexp to match Javadoc tags with no argument and {}. */
122    private static final Pattern MATCH_JAVADOC_NOARG_CURLY =
123            CommonUtil.createPattern("\\{\\s*@(inheritDoc)\\s*\\}");
124
125    /** Stack of maps for type params. */
126    private final Deque<Map<String, AbstractClassInfo>> currentTypeParams = new ArrayDeque<>();
127
128    /** Imports details. **/
129    private final Set<String> imports = new HashSet<>();
130
131    /** Full identifier for package of the method. **/
132    private FullIdent packageFullIdent;
133
134    /** Name of current class. */
135    private String currentClassName;
136
137    /** {@code ClassResolver} instance for current tree. */
138    private ClassResolver classResolver;
139
140    /** The visibility scope where Javadoc comments are checked. */
141    private Scope scope = Scope.PRIVATE;
142
143    /** The visibility scope where Javadoc comments shouldn't be checked. */
144    private Scope excludeScope;
145
146    /**
147     * Controls whether to allow documented exceptions that are not declared if
148     * they are a subclass of java.lang.RuntimeException.
149     */
150    // -@cs[AbbreviationAsWordInName] We can not change it as,
151    // check's property is part of API (used in configurations).
152    private boolean allowUndeclaredRTE;
153
154    /**
155     * Allows validating throws tags.
156     */
157    private boolean validateThrows;
158
159    /**
160     * Controls whether to allow documented exceptions that are subclass of one
161     * of declared exception. Defaults to false (backward compatibility).
162     */
163    private boolean allowThrowsTagsForSubclasses;
164
165    /**
166     * Controls whether to ignore violations when a method has parameters but does
167     * not have matching param tags in the javadoc. Defaults to false.
168     */
169    private boolean allowMissingParamTags;
170
171    /**
172     * Controls whether to ignore violations when a method declares that it throws
173     * exceptions but does not have matching throws tags in the javadoc.
174     * Defaults to false.
175     */
176    private boolean allowMissingThrowsTags;
177
178    /**
179     * Controls whether to ignore violations when a method returns non-void type
180     * but does not have a return tag in the javadoc. Defaults to false.
181     */
182    private boolean allowMissingReturnTag;
183
184    /** List of annotations that allow missed documentation. */
185    private List<String> allowedAnnotations = Collections.singletonList("Override");
186
187    /**
188     * Allow validating throws tag.
189     * @param value user's value.
190     */
191    public void setValidateThrows(boolean value) {
192        validateThrows = value;
193    }
194
195    /**
196     * Sets list of annotations.
197     * @param userAnnotations user's value.
198     */
199    public void setAllowedAnnotations(String... userAnnotations) {
200        allowedAnnotations = Arrays.asList(userAnnotations);
201    }
202
203    /**
204     * Set the scope.
205     *
206     * @param scope a scope.
207     */
208    public void setScope(Scope scope) {
209        this.scope = scope;
210    }
211
212    /**
213     * Set the excludeScope.
214     *
215     * @param excludeScope a scope.
216     */
217    public void setExcludeScope(Scope excludeScope) {
218        this.excludeScope = excludeScope;
219    }
220
221    /**
222     * Controls whether to allow documented exceptions that are not declared if
223     * they are a subclass of java.lang.RuntimeException.
224     *
225     * @param flag a {@code Boolean} value
226     */
227    // -@cs[AbbreviationAsWordInName] We can not change it as,
228    // check's property is part of API (used in configurations).
229    public void setAllowUndeclaredRTE(boolean flag) {
230        allowUndeclaredRTE = flag;
231    }
232
233    /**
234     * Controls whether to allow documented exception that are subclass of one
235     * of declared exceptions.
236     *
237     * @param flag a {@code Boolean} value
238     */
239    public void setAllowThrowsTagsForSubclasses(boolean flag) {
240        allowThrowsTagsForSubclasses = flag;
241    }
242
243    /**
244     * Controls whether to allow a method which has parameters to omit matching
245     * param tags in the javadoc. Defaults to false.
246     *
247     * @param flag a {@code Boolean} value
248     */
249    public void setAllowMissingParamTags(boolean flag) {
250        allowMissingParamTags = flag;
251    }
252
253    /**
254     * Controls whether to allow a method which declares that it throws
255     * exceptions to omit matching throws tags in the javadoc. Defaults to
256     * false.
257     *
258     * @param flag a {@code Boolean} value
259     */
260    public void setAllowMissingThrowsTags(boolean flag) {
261        allowMissingThrowsTags = flag;
262    }
263
264    /**
265     * Controls whether to allow a method which returns non-void type to omit
266     * the return tag in the javadoc. Defaults to false.
267     *
268     * @param flag a {@code Boolean} value
269     */
270    public void setAllowMissingReturnTag(boolean flag) {
271        allowMissingReturnTag = flag;
272    }
273
274    /**
275     * Controls whether to log class loading errors to the checkstyle report
276     * instead of throwing a RTE.
277     *
278     * @param logLoadErrors true if errors should be logged
279     * @deprecated No substitute.
280     */
281    @Deprecated
282    public final void setLogLoadErrors(boolean logLoadErrors) {
283        // no code
284    }
285
286    /**
287     * Controls whether to show class loading errors in the checkstyle report.
288     *
289     * @param suppressLoadErrors true if errors shouldn't be shown
290     * @deprecated No substitute.
291     */
292    @Deprecated
293    public final void setSuppressLoadErrors(boolean suppressLoadErrors) {
294        // no code
295    }
296
297    @Override
298    public final int[] getRequiredTokens() {
299        return new int[] {
300            TokenTypes.PACKAGE_DEF,
301            TokenTypes.IMPORT,
302            TokenTypes.CLASS_DEF,
303            TokenTypes.INTERFACE_DEF,
304            TokenTypes.ENUM_DEF,
305        };
306    }
307
308    @Override
309    public int[] getDefaultTokens() {
310        return getAcceptableTokens();
311    }
312
313    @Override
314    public int[] getAcceptableTokens() {
315        return new int[] {
316            TokenTypes.PACKAGE_DEF,
317            TokenTypes.IMPORT,
318            TokenTypes.CLASS_DEF,
319            TokenTypes.ENUM_DEF,
320            TokenTypes.INTERFACE_DEF,
321            TokenTypes.METHOD_DEF,
322            TokenTypes.CTOR_DEF,
323            TokenTypes.ANNOTATION_FIELD_DEF,
324        };
325    }
326
327    @Override
328    public void beginTree(DetailAST rootAST) {
329        packageFullIdent = FullIdent.createFullIdent(null);
330        imports.clear();
331        // add java.lang.* since it's always imported
332        imports.add("java.lang.*");
333        classResolver = null;
334        currentClassName = "";
335        currentTypeParams.clear();
336    }
337
338    @Override
339    public final void visitToken(DetailAST ast) {
340        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
341            processPackage(ast);
342        }
343        else if (ast.getType() == TokenTypes.IMPORT) {
344            processImport(ast);
345        }
346        else if (ast.getType() == TokenTypes.CLASS_DEF
347                 || ast.getType() == TokenTypes.INTERFACE_DEF
348                 || ast.getType() == TokenTypes.ENUM_DEF) {
349            processClass(ast);
350        }
351        else {
352            if (ast.getType() == TokenTypes.METHOD_DEF) {
353                processTypeParams(ast);
354            }
355            processAST(ast);
356        }
357    }
358
359    @Override
360    public final void leaveToken(DetailAST ast) {
361        if (ast.getType() == TokenTypes.CLASS_DEF
362            || ast.getType() == TokenTypes.INTERFACE_DEF
363            || ast.getType() == TokenTypes.ENUM_DEF) {
364            // perhaps it was inner class
365            int dotIdx = currentClassName.lastIndexOf('$');
366            if (dotIdx == -1) {
367                // perhaps just a class
368                dotIdx = currentClassName.lastIndexOf('.');
369            }
370            if (dotIdx == -1) {
371                // looks like a topmost class
372                currentClassName = "";
373            }
374            else {
375                currentClassName = currentClassName.substring(0, dotIdx);
376            }
377            currentTypeParams.pop();
378        }
379        else if (ast.getType() == TokenTypes.METHOD_DEF) {
380            currentTypeParams.pop();
381        }
382    }
383
384    /**
385     * Called to process an AST when visiting it.
386     * @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or
387     *             IMPORT tokens.
388     */
389    private void processAST(DetailAST ast) {
390        final Scope theScope = calculateScope(ast);
391        if (shouldCheck(ast, theScope)) {
392            final FileContents contents = getFileContents();
393            final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo());
394
395            if (textBlock != null) {
396                checkComment(ast, textBlock);
397            }
398        }
399    }
400
401    /**
402     * Whether we should check this node.
403     *
404     * @param ast a given node.
405     * @param nodeScope the scope of the node.
406     * @return whether we should check a given node.
407     */
408    private boolean shouldCheck(final DetailAST ast, final Scope nodeScope) {
409        final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
410
411        return (excludeScope == null
412                || nodeScope != excludeScope
413                && surroundingScope != excludeScope)
414            && nodeScope.isIn(scope)
415            && surroundingScope.isIn(scope);
416    }
417
418    /**
419     * Checks the Javadoc for a method.
420     *
421     * @param ast the token for the method
422     * @param comment the Javadoc comment
423     */
424    private void checkComment(DetailAST ast, TextBlock comment) {
425        final List<JavadocTag> tags = getMethodTags(comment);
426
427        if (!hasShortCircuitTag(ast, tags)) {
428            if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) {
429                checkReturnTag(tags, ast.getLineNo(), true);
430            }
431            else {
432                final Iterator<JavadocTag> it = tags.iterator();
433                // Check for inheritDoc
434                boolean hasInheritDocTag = false;
435                while (!hasInheritDocTag && it.hasNext()) {
436                    hasInheritDocTag = it.next().isInheritDocTag();
437                }
438                final boolean reportExpectedTags = !hasInheritDocTag
439                    && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
440
441                checkParamTags(tags, ast, reportExpectedTags);
442                checkThrowsTags(tags, getThrows(ast), reportExpectedTags);
443                if (CheckUtil.isNonVoidMethod(ast)) {
444                    checkReturnTag(tags, ast.getLineNo(), reportExpectedTags);
445                }
446            }
447
448            // Dump out all unused tags
449            tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag())
450                .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL));
451        }
452    }
453
454    /**
455     * Validates whether the Javadoc has a short circuit tag. Currently this is
456     * the inheritTag. Any violations are logged.
457     *
458     * @param ast the construct being checked
459     * @param tags the list of Javadoc tags associated with the construct
460     * @return true if the construct has a short circuit tag.
461     */
462    private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) {
463        boolean result = true;
464        // Check if it contains {@inheritDoc} tag
465        if (tags.size() == 1
466                && tags.get(0).isInheritDocTag()) {
467            // Invalid if private, a constructor, or a static method
468            if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) {
469                log(ast, MSG_INVALID_INHERIT_DOC);
470            }
471        }
472        else {
473            result = false;
474        }
475        return result;
476    }
477
478    /**
479     * Returns the scope for the method/constructor at the specified AST. If
480     * the method is in an interface or annotation block, the scope is assumed
481     * to be public.
482     *
483     * @param ast the token of the method/constructor
484     * @return the scope of the method/constructor
485     */
486    private static Scope calculateScope(final DetailAST ast) {
487        final Scope scope;
488
489        if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
490            scope = Scope.PUBLIC;
491        }
492        else {
493            final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
494            scope = ScopeUtil.getScopeFromMods(mods);
495        }
496        return scope;
497    }
498
499    /**
500     * Returns the tags in a javadoc comment. Only finds throws, exception,
501     * param, return and see tags.
502     *
503     * @param comment the Javadoc comment
504     * @return the tags found
505     */
506    private static List<JavadocTag> getMethodTags(TextBlock comment) {
507        final String[] lines = comment.getText();
508        final List<JavadocTag> tags = new ArrayList<>();
509        int currentLine = comment.getStartLineNo() - 1;
510        final int startColumnNumber = comment.getStartColNo();
511
512        for (int i = 0; i < lines.length; i++) {
513            currentLine++;
514            final Matcher javadocArgMatcher =
515                MATCH_JAVADOC_ARG.matcher(lines[i]);
516            final Matcher javadocNoargMatcher =
517                MATCH_JAVADOC_NOARG.matcher(lines[i]);
518            final Matcher noargCurlyMatcher =
519                MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]);
520            final Matcher argMultilineStart =
521                MATCH_JAVADOC_ARG_MULTILINE_START.matcher(lines[i]);
522            final Matcher noargMultilineStart =
523                MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]);
524
525            if (javadocArgMatcher.find()) {
526                final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber);
527                tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1),
528                        javadocArgMatcher.group(2)));
529            }
530            else if (javadocNoargMatcher.find()) {
531                final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber);
532                tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1)));
533            }
534            else if (noargCurlyMatcher.find()) {
535                final int col = calculateTagColumn(noargCurlyMatcher, i, startColumnNumber);
536                tags.add(new JavadocTag(currentLine, col, noargCurlyMatcher.group(1)));
537            }
538            else if (argMultilineStart.find()) {
539                final int col = calculateTagColumn(argMultilineStart, i, startColumnNumber);
540                tags.addAll(getMultilineArgTags(argMultilineStart, col, lines, i, currentLine));
541            }
542            else if (noargMultilineStart.find()) {
543                tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine));
544            }
545        }
546        return tags;
547    }
548
549    /**
550     * Calculates column number using Javadoc tag matcher.
551     * @param javadocTagMatcher found javadoc tag matcher
552     * @param lineNumber line number of Javadoc tag in comment
553     * @param startColumnNumber column number of Javadoc comment beginning
554     * @return column number
555     */
556    private static int calculateTagColumn(Matcher javadocTagMatcher,
557            int lineNumber, int startColumnNumber) {
558        int col = javadocTagMatcher.start(1) - 1;
559        if (lineNumber == 0) {
560            col += startColumnNumber;
561        }
562        return col;
563    }
564
565    /**
566     * Gets multiline Javadoc tags with arguments.
567     * @param argMultilineStart javadoc tag Matcher
568     * @param column column number of Javadoc tag
569     * @param lines comment text lines
570     * @param lineIndex line number that contains the javadoc tag
571     * @param tagLine javadoc tag line number in file
572     * @return javadoc tags with arguments
573     */
574    private static List<JavadocTag> getMultilineArgTags(final Matcher argMultilineStart,
575            final int column, final String[] lines, final int lineIndex, final int tagLine) {
576        final List<JavadocTag> tags = new ArrayList<>();
577        final String param1 = argMultilineStart.group(1);
578        final String param2 = argMultilineStart.group(2);
579        for (int remIndex = lineIndex + 1; remIndex < lines.length; remIndex++) {
580            final Matcher multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]);
581            if (multilineCont.find()) {
582                final String lFin = multilineCont.group(1);
583                if (!lFin.equals(NEXT_TAG)
584                    && !lFin.equals(END_JAVADOC)) {
585                    tags.add(new JavadocTag(tagLine, column, param1, param2));
586                }
587                break;
588            }
589        }
590
591        return tags;
592    }
593
594    /**
595     * Gets multiline Javadoc tags with no arguments.
596     * @param noargMultilineStart javadoc tag Matcher
597     * @param lines comment text lines
598     * @param lineIndex line number that contains the javadoc tag
599     * @param tagLine javadoc tag line number in file
600     * @return javadoc tags with no arguments
601     */
602    private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart,
603            final String[] lines, final int lineIndex, final int tagLine) {
604        int remIndex = lineIndex;
605        Matcher multilineCont;
606
607        do {
608            remIndex++;
609            multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]);
610        } while (!multilineCont.find());
611
612        final List<JavadocTag> tags = new ArrayList<>();
613        final String lFin = multilineCont.group(1);
614        if (!lFin.equals(NEXT_TAG)
615            && !lFin.equals(END_JAVADOC)) {
616            final String param1 = noargMultilineStart.group(1);
617            final int col = noargMultilineStart.start(1) - 1;
618
619            tags.add(new JavadocTag(tagLine, col, param1));
620        }
621
622        return tags;
623    }
624
625    /**
626     * Computes the parameter nodes for a method.
627     *
628     * @param ast the method node.
629     * @return the list of parameter nodes for ast.
630     */
631    private static List<DetailAST> getParameters(DetailAST ast) {
632        final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
633        final List<DetailAST> returnValue = new ArrayList<>();
634
635        DetailAST child = params.getFirstChild();
636        while (child != null) {
637            if (child.getType() == TokenTypes.PARAMETER_DEF) {
638                final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
639                if (ident != null) {
640                    returnValue.add(ident);
641                }
642            }
643            child = child.getNextSibling();
644        }
645        return returnValue;
646    }
647
648    /**
649     * Computes the exception nodes for a method.
650     *
651     * @param ast the method node.
652     * @return the list of exception nodes for ast.
653     */
654    private List<ExceptionInfo> getThrows(DetailAST ast) {
655        final List<ExceptionInfo> returnValue = new ArrayList<>();
656        final DetailAST throwsAST = ast
657                .findFirstToken(TokenTypes.LITERAL_THROWS);
658        if (throwsAST != null) {
659            DetailAST child = throwsAST.getFirstChild();
660            while (child != null) {
661                if (child.getType() == TokenTypes.IDENT
662                        || child.getType() == TokenTypes.DOT) {
663                    final FullIdent ident = FullIdent.createFullIdent(child);
664                    final ExceptionInfo exceptionInfo = new ExceptionInfo(
665                            createClassInfo(new Token(ident), currentClassName));
666                    returnValue.add(exceptionInfo);
667                }
668                child = child.getNextSibling();
669            }
670        }
671        return returnValue;
672    }
673
674    /**
675     * Checks a set of tags for matching parameters.
676     *
677     * @param tags the tags to check
678     * @param parent the node which takes the parameters
679     * @param reportExpectedTags whether we should report if do not find
680     *            expected tag
681     */
682    private void checkParamTags(final List<JavadocTag> tags,
683            final DetailAST parent, boolean reportExpectedTags) {
684        final List<DetailAST> params = getParameters(parent);
685        final List<DetailAST> typeParams = CheckUtil
686                .getTypeParameters(parent);
687
688        // Loop over the tags, checking to see they exist in the params.
689        final ListIterator<JavadocTag> tagIt = tags.listIterator();
690        while (tagIt.hasNext()) {
691            final JavadocTag tag = tagIt.next();
692
693            if (!tag.isParamTag()) {
694                continue;
695            }
696
697            tagIt.remove();
698
699            final String arg1 = tag.getFirstArg();
700            boolean found = removeMatchingParam(params, arg1);
701
702            if (CommonUtil.startsWithChar(arg1, '<') && CommonUtil.endsWithChar(arg1, '>')) {
703                found = searchMatchingTypeParameter(typeParams,
704                        arg1.substring(1, arg1.length() - 1));
705            }
706
707            // Handle extra JavadocTag
708            if (!found) {
709                log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
710                        "@param", arg1);
711            }
712        }
713
714        // Now dump out all type parameters/parameters without tags :- unless
715        // the user has chosen to suppress these problems
716        if (!allowMissingParamTags && reportExpectedTags) {
717            for (DetailAST param : params) {
718                log(param, MSG_EXPECTED_TAG,
719                    JavadocTagInfo.PARAM.getText(), param.getText());
720            }
721
722            for (DetailAST typeParam : typeParams) {
723                log(typeParam, MSG_EXPECTED_TAG,
724                    JavadocTagInfo.PARAM.getText(),
725                    "<" + typeParam.findFirstToken(TokenTypes.IDENT).getText()
726                    + ">");
727            }
728        }
729    }
730
731    /**
732     * Returns true if required type found in type parameters.
733     * @param typeParams
734     *            list of type parameters
735     * @param requiredTypeName
736     *            name of required type
737     * @return true if required type found in type parameters.
738     */
739    private static boolean searchMatchingTypeParameter(List<DetailAST> typeParams,
740            String requiredTypeName) {
741        // Loop looking for matching type param
742        final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
743        boolean found = false;
744        while (typeParamsIt.hasNext()) {
745            final DetailAST typeParam = typeParamsIt.next();
746            if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
747                    .equals(requiredTypeName)) {
748                found = true;
749                typeParamsIt.remove();
750                break;
751            }
752        }
753        return found;
754    }
755
756    /**
757     * Remove parameter from params collection by name.
758     * @param params collection of DetailAST parameters
759     * @param paramName name of parameter
760     * @return true if parameter found and removed
761     */
762    private static boolean removeMatchingParam(List<DetailAST> params, String paramName) {
763        boolean found = false;
764        final Iterator<DetailAST> paramIt = params.iterator();
765        while (paramIt.hasNext()) {
766            final DetailAST param = paramIt.next();
767            if (param.getText().equals(paramName)) {
768                found = true;
769                paramIt.remove();
770                break;
771            }
772        }
773        return found;
774    }
775
776    /**
777     * Checks for only one return tag. All return tags will be removed from the
778     * supplied list.
779     *
780     * @param tags the tags to check
781     * @param lineNo the line number of the expected tag
782     * @param reportExpectedTags whether we should report if do not find
783     *            expected tag
784     */
785    private void checkReturnTag(List<JavadocTag> tags, int lineNo,
786        boolean reportExpectedTags) {
787        // Loop over tags finding return tags. After the first one, report an
788        // violation.
789        boolean found = false;
790        final ListIterator<JavadocTag> it = tags.listIterator();
791        while (it.hasNext()) {
792            final JavadocTag javadocTag = it.next();
793            if (javadocTag.isReturnTag()) {
794                if (found) {
795                    log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
796                            MSG_DUPLICATE_TAG,
797                            JavadocTagInfo.RETURN.getText());
798                }
799                found = true;
800                it.remove();
801            }
802        }
803
804        // Handle there being no @return tags :- unless
805        // the user has chosen to suppress these problems
806        if (!found && !allowMissingReturnTag && reportExpectedTags) {
807            log(lineNo, MSG_RETURN_EXPECTED);
808        }
809    }
810
811    /**
812     * Checks a set of tags for matching throws.
813     *
814     * @param tags the tags to check
815     * @param throwsList the throws to check
816     * @param reportExpectedTags whether we should report if do not find
817     *            expected tag
818     */
819    private void checkThrowsTags(List<JavadocTag> tags,
820            List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
821        // Loop over the tags, checking to see they exist in the throws.
822        // The foundThrows used for performance only
823        final Set<String> foundThrows = new HashSet<>();
824        final ListIterator<JavadocTag> tagIt = tags.listIterator();
825        while (tagIt.hasNext()) {
826            final JavadocTag tag = tagIt.next();
827
828            if (!tag.isThrowsTag()) {
829                continue;
830            }
831            tagIt.remove();
832
833            // Loop looking for matching throw
834            final String documentedEx = tag.getFirstArg();
835            final Token token = new Token(tag.getFirstArg(), tag.getLineNo(), tag
836                    .getColumnNo());
837            final AbstractClassInfo documentedClassInfo = createClassInfo(token,
838                    currentClassName);
839            final boolean found = foundThrows.contains(documentedEx)
840                    || isInThrows(throwsList, documentedClassInfo, foundThrows);
841
842            // Handle extra JavadocTag.
843            if (!found) {
844                boolean reqd = true;
845                if (allowUndeclaredRTE) {
846                    reqd = !isUnchecked(documentedClassInfo.getClazz());
847                }
848
849                if (reqd && validateThrows) {
850                    log(tag.getLineNo(), tag.getColumnNo(),
851                        MSG_UNUSED_TAG,
852                        JavadocTagInfo.THROWS.getText(), tag.getFirstArg());
853                }
854            }
855        }
856        // Now dump out all throws without tags :- unless
857        // the user has chosen to suppress these problems
858        if (!allowMissingThrowsTags && reportExpectedTags) {
859            throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
860                .forEach(exceptionInfo -> {
861                    final Token token = exceptionInfo.getName();
862                    log(token.getLineNo(), token.getColumnNo(),
863                        MSG_EXPECTED_TAG,
864                        JavadocTagInfo.THROWS.getText(), token.getText());
865                });
866        }
867    }
868
869    /**
870     * Verifies that documented exception is in throws.
871     *
872     * @param throwsList list of throws
873     * @param documentedClassInfo documented exception class info
874     * @param foundThrows previously found throws
875     * @return true if documented exception is in throws.
876     */
877    private boolean isInThrows(List<ExceptionInfo> throwsList,
878            AbstractClassInfo documentedClassInfo, Set<String> foundThrows) {
879        boolean found = false;
880        ExceptionInfo foundException = null;
881
882        // First look for matches on the exception name
883        for (ExceptionInfo exceptionInfo : throwsList) {
884            if (exceptionInfo.getName().getText().equals(
885                    documentedClassInfo.getName().getText())) {
886                found = true;
887                foundException = exceptionInfo;
888                break;
889            }
890        }
891
892        // Now match on the exception type
893        final ListIterator<ExceptionInfo> exceptionInfoIt = throwsList.listIterator();
894        while (!found && exceptionInfoIt.hasNext()) {
895            final ExceptionInfo exceptionInfo = exceptionInfoIt.next();
896
897            if (documentedClassInfo.getClazz() == exceptionInfo.getClazz()) {
898                found = true;
899                foundException = exceptionInfo;
900            }
901            else if (allowThrowsTagsForSubclasses) {
902                found = isSubclass(documentedClassInfo.getClazz(), exceptionInfo.getClazz());
903            }
904        }
905
906        if (foundException != null) {
907            foundException.setFound();
908            foundThrows.add(documentedClassInfo.getName().getText());
909        }
910
911        return found;
912    }
913
914    /**
915     * Is exception is unchecked (subclass of {@code RuntimeException}
916     * or {@code Error}.
917     *
918     * @param exception {@code Class} of exception to check
919     * @return true  if exception is unchecked
920     *         false if exception is checked
921     */
922    private static boolean isUnchecked(Class<?> exception) {
923        return isSubclass(exception, RuntimeException.class)
924            || isSubclass(exception, Error.class);
925    }
926
927    /**
928     * Checks if one class is subclass of another.
929     *
930     * @param child {@code Class} of class
931     *               which should be child
932     * @param parent {@code Class} of class
933     *                which should be parent
934     * @return true  if aChild is subclass of aParent
935     *         false otherwise
936     */
937    private static boolean isSubclass(Class<?> child, Class<?> parent) {
938        return parent != null && child != null
939            && parent.isAssignableFrom(child);
940    }
941
942    /**
943     * Returns the current tree's ClassResolver.
944     * @return {@code ClassResolver} for current tree.
945     */
946    private ClassResolver getClassResolver() {
947        if (classResolver == null) {
948            classResolver =
949                new ClassResolver(getClass().getClassLoader(),
950                                  packageFullIdent.getText(),
951                                  imports);
952        }
953        return classResolver;
954    }
955
956    /**
957     * Attempts to resolve the Class for a specified name.
958     * @param resolvableClassName name of the class to resolve
959     * @param className name of surrounding class.
960     * @return the resolved class or {@code null}
961     *          if unable to resolve the class.
962     * @noinspection WeakerAccess
963     */
964    // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
965    private Class<?> resolveClass(String resolvableClassName,
966                                          String className) {
967        Class<?> clazz;
968        try {
969            clazz = getClassResolver().resolve(resolvableClassName, className);
970        }
971        // -@cs[IllegalCatch] Exception type is not predictable.
972        catch (final Exception ignored) {
973            clazz = null;
974        }
975        return clazz;
976    }
977
978    /**
979     * Tries to load class. Logs error if unable.
980     * @param ident name of class which we try to load.
981     * @param className name of surrounding class.
982     * @return {@code Class} for a ident.
983     * @noinspection WeakerAccess, MethodOnlyUsedFromInnerClass
984     */
985    // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
986    private Class<?> tryLoadClass(Token ident, String className) {
987        return resolveClass(ident.getText(), className);
988    }
989
990    /**
991     * Collects the details of a package.
992     * @param ast node containing the package details
993     */
994    private void processPackage(DetailAST ast) {
995        final DetailAST nameAST = ast.getLastChild().getPreviousSibling();
996        packageFullIdent = FullIdent.createFullIdent(nameAST);
997    }
998
999    /**
1000     * Collects the details of imports.
1001     * @param ast node containing the import details
1002     */
1003    private void processImport(DetailAST ast) {
1004        final FullIdent name = FullIdent.createFullIdentBelow(ast);
1005        imports.add(name.getText());
1006    }
1007
1008    /**
1009     * Process type params (if any) for given class, enum or method.
1010     * @param ast class, enum or method to process.
1011     */
1012    private void processTypeParams(DetailAST ast) {
1013        final DetailAST params =
1014            ast.findFirstToken(TokenTypes.TYPE_PARAMETERS);
1015
1016        final Map<String, AbstractClassInfo> paramsMap = new HashMap<>();
1017        currentTypeParams.push(paramsMap);
1018
1019        if (params != null) {
1020            for (DetailAST child = params.getFirstChild();
1021                 child != null;
1022                 child = child.getNextSibling()) {
1023                if (child.getType() == TokenTypes.TYPE_PARAMETER) {
1024                    final DetailAST bounds =
1025                        child.findFirstToken(TokenTypes.TYPE_UPPER_BOUNDS);
1026                    if (bounds != null) {
1027                        final FullIdent name =
1028                            FullIdent.createFullIdentBelow(bounds);
1029                        final AbstractClassInfo classInfo =
1030                            createClassInfo(new Token(name), currentClassName);
1031                        final String alias =
1032                                child.findFirstToken(TokenTypes.IDENT).getText();
1033                        paramsMap.put(alias, classInfo);
1034                    }
1035                }
1036            }
1037        }
1038    }
1039
1040    /**
1041     * Processes class definition.
1042     * @param ast class definition to process.
1043     */
1044    private void processClass(DetailAST ast) {
1045        final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
1046        String innerClass = ident.getText();
1047
1048        if (!currentClassName.isEmpty()) {
1049            innerClass = "$" + innerClass;
1050        }
1051        currentClassName += innerClass;
1052        processTypeParams(ast);
1053    }
1054
1055    /**
1056     * Creates class info for given name.
1057     * @param name name of type.
1058     * @param surroundingClass name of surrounding class.
1059     * @return class info for given name.
1060     */
1061    private AbstractClassInfo createClassInfo(final Token name,
1062                                              final String surroundingClass) {
1063        final AbstractClassInfo result;
1064        final AbstractClassInfo classInfo = findClassAlias(name.getText());
1065        if (classInfo == null) {
1066            result = new RegularClass(name, surroundingClass, this);
1067        }
1068        else {
1069            result = new ClassAlias(name, classInfo);
1070        }
1071        return result;
1072    }
1073
1074    /**
1075     * Looking if a given name is alias.
1076     * @param name given name
1077     * @return ClassInfo for alias if it exists, null otherwise
1078     * @noinspection WeakerAccess
1079     */
1080    private AbstractClassInfo findClassAlias(final String name) {
1081        AbstractClassInfo classInfo = null;
1082        final Iterator<Map<String, AbstractClassInfo>> iterator = currentTypeParams
1083                .descendingIterator();
1084        while (iterator.hasNext()) {
1085            final Map<String, AbstractClassInfo> paramMap = iterator.next();
1086            classInfo = paramMap.get(name);
1087            if (classInfo != null) {
1088                break;
1089            }
1090        }
1091        return classInfo;
1092    }
1093
1094    /**
1095     * Contains class's {@code Token}.
1096     */
1097    private abstract static class AbstractClassInfo {
1098
1099        /** {@code FullIdent} associated with this class. */
1100        private final Token name;
1101
1102        /**
1103         * Creates new instance of class information object.
1104         * @param className token which represents class name.
1105         */
1106        protected AbstractClassInfo(final Token className) {
1107            if (className == null) {
1108                throw new IllegalArgumentException(
1109                    "ClassInfo's name should be non-null");
1110            }
1111            name = className;
1112        }
1113
1114        /**
1115         * Returns class associated with that object.
1116         * @return {@code Class} associated with an object.
1117         */
1118        // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
1119        public abstract Class<?> getClazz();
1120
1121        /**
1122         * Gets class name.
1123         * @return class name
1124         */
1125        public final Token getName() {
1126            return name;
1127        }
1128
1129    }
1130
1131    /** Represents regular classes/enums. */
1132    private static final class RegularClass extends AbstractClassInfo {
1133
1134        /** Name of surrounding class. */
1135        private final String surroundingClass;
1136        /** The check we use to resolve classes. */
1137        private final JavadocMethodCheck check;
1138        /** Is class loadable. */
1139        private boolean loadable = true;
1140        /** {@code Class} object of this class if it's loadable. */
1141        private Class<?> classObj;
1142
1143        /**
1144         * Creates new instance of of class information object.
1145         * @param name {@code FullIdent} associated with new object.
1146         * @param surroundingClass name of current surrounding class.
1147         * @param check the check we use to load class.
1148         */
1149        /* package */ RegularClass(final Token name,
1150                             final String surroundingClass,
1151                             final JavadocMethodCheck check) {
1152            super(name);
1153            this.surroundingClass = surroundingClass;
1154            this.check = check;
1155        }
1156
1157        @Override
1158        public Class<?> getClazz() {
1159            if (loadable && classObj == null) {
1160                setClazz(check.tryLoadClass(getName(), surroundingClass));
1161            }
1162            return classObj;
1163        }
1164
1165        /**
1166         * Associates {@code Class} with an object.
1167         * @param clazz {@code Class} to associate with.
1168         */
1169        private void setClazz(Class<?> clazz) {
1170            classObj = clazz;
1171            loadable = clazz != null;
1172        }
1173
1174        @Override
1175        public String toString() {
1176            return "RegularClass[name=" + getName()
1177                    + ", in class='" + surroundingClass + '\''
1178                    + ", check=" + check.hashCode()
1179                    + ", loadable=" + loadable
1180                    + ", class=" + classObj
1181                    + ']';
1182        }
1183
1184    }
1185
1186    /** Represents type param which is "alias" for real type. */
1187    private static class ClassAlias extends AbstractClassInfo {
1188
1189        /** Class information associated with the alias. */
1190        private final AbstractClassInfo classInfo;
1191
1192        /**
1193         * Creates new instance of the class.
1194         * @param name token which represents name of class alias.
1195         * @param classInfo class information associated with the alias.
1196         */
1197        /* package */ ClassAlias(final Token name, AbstractClassInfo classInfo) {
1198            super(name);
1199            this.classInfo = classInfo;
1200        }
1201
1202        @Override
1203        public final Class<?> getClazz() {
1204            return classInfo.getClazz();
1205        }
1206
1207        @Override
1208        public String toString() {
1209            return "ClassAlias[alias " + getName() + " for " + classInfo.getName() + "]";
1210        }
1211
1212    }
1213
1214    /**
1215     * Represents text element with location in the text.
1216     */
1217    private static class Token {
1218
1219        /** Token's column number. */
1220        private final int columnNo;
1221        /** Token's line number. */
1222        private final int lineNo;
1223        /** Token's text. */
1224        private final String text;
1225
1226        /**
1227         * Creates token.
1228         * @param text token's text
1229         * @param lineNo token's line number
1230         * @param columnNo token's column number
1231         */
1232        /* default */ Token(String text, int lineNo, int columnNo) {
1233            this.text = text;
1234            this.lineNo = lineNo;
1235            this.columnNo = columnNo;
1236        }
1237
1238        /**
1239         * Converts FullIdent to Token.
1240         * @param fullIdent full ident to convert.
1241         */
1242        /* default */ Token(FullIdent fullIdent) {
1243            text = fullIdent.getText();
1244            lineNo = fullIdent.getLineNo();
1245            columnNo = fullIdent.getColumnNo();
1246        }
1247
1248        /**
1249         * Gets line number of the token.
1250         * @return line number of the token
1251         */
1252        public int getLineNo() {
1253            return lineNo;
1254        }
1255
1256        /**
1257         * Gets column number of the token.
1258         * @return column number of the token
1259         */
1260        public int getColumnNo() {
1261            return columnNo;
1262        }
1263
1264        /**
1265         * Gets text of the token.
1266         * @return text of the token
1267         */
1268        public String getText() {
1269            return text;
1270        }
1271
1272        @Override
1273        public String toString() {
1274            return "Token[" + text + "(" + lineNo
1275                + "x" + columnNo + ")]";
1276        }
1277
1278    }
1279
1280    /** Stores useful information about declared exception. */
1281    private static class ExceptionInfo {
1282
1283        /** Class information associated with this exception. */
1284        private final AbstractClassInfo classInfo;
1285        /** Does the exception have throws tag associated with. */
1286        private boolean found;
1287
1288        /**
1289         * Creates new instance for {@code FullIdent}.
1290         *
1291         * @param classInfo class info
1292         */
1293        /* package */ ExceptionInfo(AbstractClassInfo classInfo) {
1294            this.classInfo = classInfo;
1295        }
1296
1297        /** Mark that the exception has associated throws tag. */
1298        private void setFound() {
1299            found = true;
1300        }
1301
1302        /**
1303         * Checks that the exception has throws tag associated with it.
1304         * @return whether the exception has throws tag associated with
1305         */
1306        private boolean isFound() {
1307            return found;
1308        }
1309
1310        /**
1311         * Gets exception name.
1312         * @return exception's name
1313         */
1314        private Token getName() {
1315            return classInfo.getName();
1316        }
1317
1318        /**
1319         * Gets exception class.
1320         * @return class for this exception
1321         */
1322        private Class<?> getClazz() {
1323            return classInfo.getClazz();
1324        }
1325
1326    }
1327
1328}