//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.javadoc; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.FileContents; import com.puppycrawl.tools.checkstyle.api.FullIdent; import com.puppycrawl.tools.checkstyle.api.Scope; import com.puppycrawl.tools.checkstyle.api.TextBlock; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.checks.AbstractTypeAwareCheck; import com.puppycrawl.tools.checkstyle.utils.CheckUtils; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; import com.puppycrawl.tools.checkstyle.utils.ScopeUtils; /** * Checks the Javadoc of a method or constructor. * * @author Oliver Burn * @author Rick Giles * @author o_sukhodoslky */ @SuppressWarnings("deprecation") public class JavadocMethodCheck extends AbstractTypeAwareCheck { /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_JAVADOC_MISSING = "javadoc.missing"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_CLASS_INFO = "javadoc.classInfo"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_UNUSED_TAG = "javadoc.unusedTag"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag"; /** Compiled regexp to match Javadoc tags that take an argument. */ private static final Pattern MATCH_JAVADOC_ARG = CommonUtils.createPattern("@(throws|exception|param)\\s+(\\S+)\\s+\\S*"); /** Compiled regexp to match first part of multilineJavadoc tags. */ private static final Pattern MATCH_JAVADOC_ARG_MULTILINE_START = CommonUtils.createPattern("@(throws|exception|param)\\s+(\\S+)\\s*$"); /** Compiled regexp to look for a continuation of the comment. */ private static final Pattern MATCH_JAVADOC_MULTILINE_CONT = CommonUtils.createPattern("(\\*/|@|[^\\s\\*])"); /** Multiline finished at end of comment. */ private static final String END_JAVADOC = "*/"; /** Multiline finished at next Javadoc. */ private static final String NEXT_TAG = "@"; /** Compiled regexp to match Javadoc tags with no argument. */ private static final Pattern MATCH_JAVADOC_NOARG = CommonUtils.createPattern("@(return|see)\\s+\\S"); /** Compiled regexp to match first part of multilineJavadoc tags. */ private static final Pattern MATCH_JAVADOC_NOARG_MULTILINE_START = CommonUtils.createPattern("@(return|see)\\s*$"); /** Compiled regexp to match Javadoc tags with no argument and {}. */ private static final Pattern MATCH_JAVADOC_NOARG_CURLY = CommonUtils.createPattern("\\{\\s*@(inheritDoc)\\s*\\}"); /** Default value of minimal amount of lines in method to demand documentation presence.*/ private static final int DEFAULT_MIN_LINE_COUNT = -1; /** The visibility scope where Javadoc comments are checked. */ private Scope scope = Scope.PRIVATE; /** The visibility scope where Javadoc comments shouldn't be checked. */ private Scope excludeScope; /** Minimal amount of lines in method to demand documentation presence.*/ private int minLineCount = DEFAULT_MIN_LINE_COUNT; /** * Controls whether to allow documented exceptions that are not declared if * they are a subclass of java.lang.RuntimeException. */ // -@cs[AbbreviationAsWordInName] We can not change it as, // check's property is part of API (used in configurations). private boolean allowUndeclaredRTE; /** * Allows validating throws tags. */ private boolean validateThrows; /** * Controls whether to allow documented exceptions that are subclass of one * of declared exception. Defaults to false (backward compatibility). */ private boolean allowThrowsTagsForSubclasses; /** * Controls whether to ignore errors when a method has parameters but does * not have matching param tags in the javadoc. Defaults to false. */ private boolean allowMissingParamTags; /** * Controls whether to ignore errors when a method declares that it throws * exceptions but does not have matching throws tags in the javadoc. * Defaults to false. */ private boolean allowMissingThrowsTags; /** * Controls whether to ignore errors when a method returns non-void type * but does not have a return tag in the javadoc. Defaults to false. */ private boolean allowMissingReturnTag; /** * Controls whether to ignore errors when there is no javadoc. Defaults to * false. */ private boolean allowMissingJavadoc; /** * Controls whether to allow missing Javadoc on accessor methods for * properties (setters and getters). */ private boolean allowMissingPropertyJavadoc; /** List of annotations that could allow missed documentation. */ private List<String> allowedAnnotations = Collections.singletonList("Override"); /** Method names that match this pattern do not require javadoc blocks. */ private Pattern ignoreMethodNamesRegex; /** * Set regex for matching method names to ignore. * @param pattern a pattern. */ public void setIgnoreMethodNamesRegex(Pattern pattern) { ignoreMethodNamesRegex = pattern; } /** * Sets minimal amount of lines in method. * @param value user's value. */ public void setMinLineCount(int value) { minLineCount = value; } /** * Allow validating throws tag. * @param value user's value. */ public void setValidateThrows(boolean value) { validateThrows = value; } /** * Sets list of annotations. * @param userAnnotations user's value. */ public void setAllowedAnnotations(String... userAnnotations) { allowedAnnotations = Arrays.asList(userAnnotations); } /** * Set the scope. * * @param scope a scope. */ public void setScope(Scope scope) { this.scope = scope; } /** * Set the excludeScope. * * @param excludeScope a scope. */ public void setExcludeScope(Scope excludeScope) { this.excludeScope = excludeScope; } /** * Controls whether to allow documented exceptions that are not declared if * they are a subclass of java.lang.RuntimeException. * * @param flag a {@code Boolean} value */ // -@cs[AbbreviationAsWordInName] We can not change it as, // check's property is part of API (used in configurations). public void setAllowUndeclaredRTE(boolean flag) { allowUndeclaredRTE = flag; } /** * Controls whether to allow documented exception that are subclass of one * of declared exceptions. * * @param flag a {@code Boolean} value */ public void setAllowThrowsTagsForSubclasses(boolean flag) { allowThrowsTagsForSubclasses = flag; } /** * Controls whether to allow a method which has parameters to omit matching * param tags in the javadoc. Defaults to false. * * @param flag a {@code Boolean} value */ public void setAllowMissingParamTags(boolean flag) { allowMissingParamTags = flag; } /** * Controls whether to allow a method which declares that it throws * exceptions to omit matching throws tags in the javadoc. Defaults to * false. * * @param flag a {@code Boolean} value */ public void setAllowMissingThrowsTags(boolean flag) { allowMissingThrowsTags = flag; } /** * Controls whether to allow a method which returns non-void type to omit * the return tag in the javadoc. Defaults to false. * * @param flag a {@code Boolean} value */ public void setAllowMissingReturnTag(boolean flag) { allowMissingReturnTag = flag; } /** * Controls whether to ignore errors when there is no javadoc. Defaults to * false. * * @param flag a {@code Boolean} value */ public void setAllowMissingJavadoc(boolean flag) { allowMissingJavadoc = flag; } /** * Controls whether to ignore errors when there is no javadoc for a * property accessor (setter/getter methods). Defaults to false. * * @param flag a {@code Boolean} value */ public void setAllowMissingPropertyJavadoc(final boolean flag) { allowMissingPropertyJavadoc = flag; } @Override public int[] getDefaultTokens() { return getAcceptableTokens(); } @Override public int[] getAcceptableTokens() { return new int[] { TokenTypes.PACKAGE_DEF, TokenTypes.IMPORT, TokenTypes.CLASS_DEF, TokenTypes.ENUM_DEF, TokenTypes.INTERFACE_DEF, TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF, TokenTypes.ANNOTATION_FIELD_DEF, }; } @Override public boolean isCommentNodesRequired() { return true; } @Override protected final void processAST(DetailAST ast) { final Scope theScope = calculateScope(ast); if (shouldCheck(ast, theScope)) { final FileContents contents = getFileContents(); final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo()); if (textBlock == null) { if (!isMissingJavadocAllowed(ast)) { log(ast, MSG_JAVADOC_MISSING); } } else { checkComment(ast, textBlock); } } } /** * Some javadoc. * @param methodDef Some javadoc. * @return Some javadoc. */ private boolean hasAllowedAnnotations(DetailAST methodDef) { boolean result = false; final DetailAST modifiersNode = methodDef.findFirstToken(TokenTypes.MODIFIERS); DetailAST annotationNode = modifiersNode.findFirstToken(TokenTypes.ANNOTATION); while (annotationNode != null && annotationNode.getType() == TokenTypes.ANNOTATION) { DetailAST identNode = annotationNode.findFirstToken(TokenTypes.IDENT); if (identNode == null) { identNode = annotationNode.findFirstToken(TokenTypes.DOT) .findFirstToken(TokenTypes.IDENT); } if (allowedAnnotations.contains(identNode.getText())) { result = true; break; } annotationNode = annotationNode.getNextSibling(); } return result; } /** * Some javadoc. * @param methodDef Some javadoc. * @return Some javadoc. */ private static int getMethodsNumberOfLine(DetailAST methodDef) { final int numberOfLines; final DetailAST lcurly = methodDef.getLastChild(); final DetailAST rcurly = lcurly.getLastChild(); if (lcurly.getFirstChild() == rcurly) { numberOfLines = 1; } else { numberOfLines = rcurly.getLineNo() - lcurly.getLineNo() - 1; } return numberOfLines; } @Override protected final void logLoadError(Token ident) { logLoadErrorImpl(ident.getLineNo(), ident.getColumnNo(), MSG_CLASS_INFO, JavadocTagInfo.THROWS.getText(), ident.getText()); } /** * The JavadocMethodCheck is about to report a missing Javadoc. * This hook can be used by derived classes to allow a missing javadoc * in some situations. The default implementation checks * {@code allowMissingJavadoc} and * {@code allowMissingPropertyJavadoc} properties, do not forget * to call {@code super.isMissingJavadocAllowed(ast)} in case * you want to keep this logic. * @param ast the tree node for the method or constructor. * @return True if this method or constructor doesn't need Javadoc. */ protected boolean isMissingJavadocAllowed(final DetailAST ast) { return allowMissingJavadoc || allowMissingPropertyJavadoc && (CheckUtils.isSetterMethod(ast) || CheckUtils.isGetterMethod(ast)) || matchesSkipRegex(ast) || isContentsAllowMissingJavadoc(ast); } /** * Checks if the Javadoc can be missing if the method or constructor is * below the minimum line count or has a special annotation. * * @param ast the tree node for the method or constructor. * @return True if this method or constructor doesn't need Javadoc. */ private boolean isContentsAllowMissingJavadoc(DetailAST ast) { return (ast.getType() == TokenTypes.METHOD_DEF || ast.getType() == TokenTypes.CTOR_DEF) && (getMethodsNumberOfLine(ast) <= minLineCount || hasAllowedAnnotations(ast)); } /** * Checks if the given method name matches the regex. In that case * we skip enforcement of javadoc for this method * @param methodDef {@link TokenTypes#METHOD_DEF METHOD_DEF} * @return true if given method name matches the regex. */ private boolean matchesSkipRegex(DetailAST methodDef) { boolean result = false; if (ignoreMethodNamesRegex != null) { final DetailAST ident = methodDef.findFirstToken(TokenTypes.IDENT); final String methodName = ident.getText(); final Matcher matcher = ignoreMethodNamesRegex.matcher(methodName); if (matcher.matches()) { result = true; } } return result; } /** * Whether we should check this node. * * @param ast a given node. * @param nodeScope the scope of the node. * @return whether we should check a given node. */ private boolean shouldCheck(final DetailAST ast, final Scope nodeScope) { final Scope surroundingScope = ScopeUtils.getSurroundingScope(ast); return (excludeScope == null || nodeScope != excludeScope && surroundingScope != excludeScope) && nodeScope.isIn(scope) && surroundingScope.isIn(scope); } /** * Checks the Javadoc for a method. * * @param ast the token for the method * @param comment the Javadoc comment */ private void checkComment(DetailAST ast, TextBlock comment) { final List<JavadocTag> tags = getMethodTags(comment); if (!hasShortCircuitTag(ast, tags)) { if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) { checkReturnTag(tags, ast.getLineNo(), true); } else { final Iterator<JavadocTag> it = tags.iterator(); // Check for inheritDoc boolean hasInheritDocTag = false; while (!hasInheritDocTag && it.hasNext()) { hasInheritDocTag = it.next().isInheritDocTag(); } final boolean reportExpectedTags = !hasInheritDocTag && !hasAllowedAnnotations(ast); checkParamTags(tags, ast, reportExpectedTags); checkThrowsTags(tags, getThrows(ast), reportExpectedTags); if (CheckUtils.isNonVoidMethod(ast)) { checkReturnTag(tags, ast.getLineNo(), reportExpectedTags); } } // Dump out all unused tags tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag()) .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL)); } } /** * Validates whether the Javadoc has a short circuit tag. Currently this is * the inheritTag. Any errors are logged. * * @param ast the construct being checked * @param tags the list of Javadoc tags associated with the construct * @return true if the construct has a short circuit tag. */ private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) { boolean result = true; // Check if it contains {@inheritDoc} tag if (tags.size() == 1 && tags.get(0).isInheritDocTag()) { // Invalid if private, a constructor, or a static method if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) { log(ast, MSG_INVALID_INHERIT_DOC); } } else { result = false; } return result; } /** * Returns the scope for the method/constructor at the specified AST. If * the method is in an interface or annotation block, the scope is assumed * to be public. * * @param ast the token of the method/constructor * @return the scope of the method/constructor */ private static Scope calculateScope(final DetailAST ast) { final Scope scope; if (ScopeUtils.isInInterfaceOrAnnotationBlock(ast)) { scope = Scope.PUBLIC; } else { final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); scope = ScopeUtils.getScopeFromMods(mods); } return scope; } /** * Returns the tags in a javadoc comment. Only finds throws, exception, * param, return and see tags. * * @param comment the Javadoc comment * @return the tags found */ private static List<JavadocTag> getMethodTags(TextBlock comment) { final String[] lines = comment.getText(); final List<JavadocTag> tags = new ArrayList<>(); int currentLine = comment.getStartLineNo() - 1; final int startColumnNumber = comment.getStartColNo(); for (int i = 0; i < lines.length; i++) { currentLine++; final Matcher javadocArgMatcher = MATCH_JAVADOC_ARG.matcher(lines[i]); final Matcher javadocNoargMatcher = MATCH_JAVADOC_NOARG.matcher(lines[i]); final Matcher noargCurlyMatcher = MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]); final Matcher argMultilineStart = MATCH_JAVADOC_ARG_MULTILINE_START.matcher(lines[i]); final Matcher noargMultilineStart = MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]); if (javadocArgMatcher.find()) { final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber); tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1), javadocArgMatcher.group(2))); } else if (javadocNoargMatcher.find()) { final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber); tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1))); } else if (noargCurlyMatcher.find()) { final int col = calculateTagColumn(noargCurlyMatcher, i, startColumnNumber); tags.add(new JavadocTag(currentLine, col, noargCurlyMatcher.group(1))); } else if (argMultilineStart.find()) { final int col = calculateTagColumn(argMultilineStart, i, startColumnNumber); tags.addAll(getMultilineArgTags(argMultilineStart, col, lines, i, currentLine)); } else if (noargMultilineStart.find()) { tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine)); } } return tags; } /** * Calculates column number using Javadoc tag matcher. * @param javadocTagMatcher found javadoc tag matcher * @param lineNumber line number of Javadoc tag in comment * @param startColumnNumber column number of Javadoc comment beginning * @return column number */ private static int calculateTagColumn(Matcher javadocTagMatcher, int lineNumber, int startColumnNumber) { int col = javadocTagMatcher.start(1) - 1; if (lineNumber == 0) { col += startColumnNumber; } return col; } /** * Gets multiline Javadoc tags with arguments. * @param argMultilineStart javadoc tag Matcher * @param column column number of Javadoc tag * @param lines comment text lines * @param lineIndex line number that contains the javadoc tag * @param tagLine javadoc tag line number in file * @return javadoc tags with arguments */ private static List<JavadocTag> getMultilineArgTags(final Matcher argMultilineStart, final int column, final String[] lines, final int lineIndex, final int tagLine) { final List<JavadocTag> tags = new ArrayList<>(); final String param1 = argMultilineStart.group(1); final String param2 = argMultilineStart.group(2); int remIndex = lineIndex + 1; while (remIndex < lines.length) { final Matcher multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]); if (multilineCont.find()) { remIndex = lines.length; final String lFin = multilineCont.group(1); if (!lFin.equals(NEXT_TAG) && !lFin.equals(END_JAVADOC)) { tags.add(new JavadocTag(tagLine, column, param1, param2)); } } remIndex++; } return tags; } /** * Gets multiline Javadoc tags with no arguments. * @param noargMultilineStart javadoc tag Matcher * @param lines comment text lines * @param lineIndex line number that contains the javadoc tag * @param tagLine javadoc tag line number in file * @return javadoc tags with no arguments */ private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart, final String[] lines, final int lineIndex, final int tagLine) { final String param1 = noargMultilineStart.group(1); final int col = noargMultilineStart.start(1) - 1; final List<JavadocTag> tags = new ArrayList<>(); int remIndex = lineIndex + 1; while (remIndex < lines.length) { final Matcher multilineCont = MATCH_JAVADOC_MULTILINE_CONT .matcher(lines[remIndex]); if (multilineCont.find()) { remIndex = lines.length; final String lFin = multilineCont.group(1); if (!lFin.equals(NEXT_TAG) && !lFin.equals(END_JAVADOC)) { tags.add(new JavadocTag(tagLine, col, param1)); } } remIndex++; } return tags; } /** * Computes the parameter nodes for a method. * * @param ast the method node. * @return the list of parameter nodes for ast. */ private static List<DetailAST> getParameters(DetailAST ast) { final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); final List<DetailAST> returnValue = new ArrayList<>(); DetailAST child = params.getFirstChild(); while (child != null) { if (child.getType() == TokenTypes.PARAMETER_DEF) { final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); if (ident != null) { returnValue.add(ident); } } child = child.getNextSibling(); } return returnValue; } /** * Computes the exception nodes for a method. * * @param ast the method node. * @return the list of exception nodes for ast. */ private List<ExceptionInfo> getThrows(DetailAST ast) { final List<ExceptionInfo> returnValue = new ArrayList<>(); final DetailAST throwsAST = ast .findFirstToken(TokenTypes.LITERAL_THROWS); if (throwsAST != null) { DetailAST child = throwsAST.getFirstChild(); while (child != null) { if (child.getType() == TokenTypes.IDENT || child.getType() == TokenTypes.DOT) { final FullIdent ident = FullIdent.createFullIdent(child); final ExceptionInfo exceptionInfo = new ExceptionInfo( createClassInfo(new Token(ident), getCurrentClassName())); returnValue.add(exceptionInfo); } child = child.getNextSibling(); } } return returnValue; } /** * Checks a set of tags for matching parameters. * * @param tags the tags to check * @param parent the node which takes the parameters * @param reportExpectedTags whether we should report if do not find * expected tag */ private void checkParamTags(final List<JavadocTag> tags, final DetailAST parent, boolean reportExpectedTags) { final List<DetailAST> params = getParameters(parent); final List<DetailAST> typeParams = CheckUtils .getTypeParameters(parent); // Loop over the tags, checking to see they exist in the params. final ListIterator<JavadocTag> tagIt = tags.listIterator(); while (tagIt.hasNext()) { final JavadocTag tag = tagIt.next(); if (!tag.isParamTag()) { continue; } tagIt.remove(); final String arg1 = tag.getFirstArg(); boolean found = removeMatchingParam(params, arg1); if (CommonUtils.startsWithChar(arg1, '<') && CommonUtils.endsWithChar(arg1, '>')) { found = searchMatchingTypeParameter(typeParams, arg1.substring(1, arg1.length() - 1)); } // Handle extra JavadocTag if (!found) { log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, "@param", arg1); } } // Now dump out all type parameters/parameters without tags :- unless // the user has chosen to suppress these problems if (!allowMissingParamTags && reportExpectedTags) { for (DetailAST param : params) { log(param, MSG_EXPECTED_TAG, JavadocTagInfo.PARAM.getText(), param.getText()); } for (DetailAST typeParam : typeParams) { log(typeParam, MSG_EXPECTED_TAG, JavadocTagInfo.PARAM.getText(), "<" + typeParam.findFirstToken(TokenTypes.IDENT).getText() + ">"); } } } /** * Returns true if required type found in type parameters. * @param typeParams * list of type parameters * @param requiredTypeName * name of required type * @return true if required type found in type parameters. */ private static boolean searchMatchingTypeParameter(List<DetailAST> typeParams, String requiredTypeName) { // Loop looking for matching type param final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); boolean found = false; while (typeParamsIt.hasNext()) { final DetailAST typeParam = typeParamsIt.next(); if (typeParam.findFirstToken(TokenTypes.IDENT).getText() .equals(requiredTypeName)) { found = true; typeParamsIt.remove(); break; } } return found; } /** * Remove parameter from params collection by name. * @param params collection of DetailAST parameters * @param paramName name of parameter * @return true if parameter found and removed */ private static boolean removeMatchingParam(List<DetailAST> params, String paramName) { boolean found = false; final Iterator<DetailAST> paramIt = params.iterator(); while (paramIt.hasNext()) { final DetailAST param = paramIt.next(); if (param.getText().equals(paramName)) { found = true; paramIt.remove(); break; } } return found; } /** * Checks for only one return tag. All return tags will be removed from the * supplied list. * * @param tags the tags to check * @param lineNo the line number of the expected tag * @param reportExpectedTags whether we should report if do not find * expected tag */ private void checkReturnTag(List<JavadocTag> tags, int lineNo, boolean reportExpectedTags) { // Loop over tags finding return tags. After the first one, report an // error. boolean found = false; final ListIterator<JavadocTag> it = tags.listIterator(); while (it.hasNext()) { final JavadocTag javadocTag = it.next(); if (javadocTag.isReturnTag()) { if (found) { log(javadocTag.getLineNo(), javadocTag.getColumnNo(), MSG_DUPLICATE_TAG, JavadocTagInfo.RETURN.getText()); } found = true; it.remove(); } } // Handle there being no @return tags :- unless // the user has chosen to suppress these problems if (!found && !allowMissingReturnTag && reportExpectedTags) { log(lineNo, MSG_RETURN_EXPECTED); } } /** * Checks a set of tags for matching throws. * * @param tags the tags to check * @param throwsList the throws to check * @param reportExpectedTags whether we should report if do not find * expected tag */ private void checkThrowsTags(List<JavadocTag> tags, List<ExceptionInfo> throwsList, boolean reportExpectedTags) { // Loop over the tags, checking to see they exist in the throws. // The foundThrows used for performance only final Set<String> foundThrows = new HashSet<>(); final ListIterator<JavadocTag> tagIt = tags.listIterator(); while (tagIt.hasNext()) { final JavadocTag tag = tagIt.next(); if (!tag.isThrowsTag()) { continue; } tagIt.remove(); // Loop looking for matching throw final String documentedEx = tag.getFirstArg(); final Token token = new Token(tag.getFirstArg(), tag.getLineNo(), tag .getColumnNo()); final AbstractClassInfo documentedClassInfo = createClassInfo(token, getCurrentClassName()); final boolean found = foundThrows.contains(documentedEx) || isInThrows(throwsList, documentedClassInfo, foundThrows); // Handle extra JavadocTag. if (!found) { boolean reqd = true; if (allowUndeclaredRTE) { reqd = !isUnchecked(documentedClassInfo.getClazz()); } if (reqd && validateThrows) { log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, JavadocTagInfo.THROWS.getText(), tag.getFirstArg()); } } } // Now dump out all throws without tags :- unless // the user has chosen to suppress these problems if (!allowMissingThrowsTags && reportExpectedTags) { throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) .forEach(exceptionInfo -> { final Token token = exceptionInfo.getName(); log(token.getLineNo(), token.getColumnNo(), MSG_EXPECTED_TAG, JavadocTagInfo.THROWS.getText(), token.getText()); }); } } /** * Verifies that documented exception is in throws. * * @param throwsList list of throws * @param documentedClassInfo documented exception class info * @param foundThrows previously found throws * @return true if documented exception is in throws. */ private boolean isInThrows(List<ExceptionInfo> throwsList, AbstractClassInfo documentedClassInfo, Set<String> foundThrows) { boolean found = false; ExceptionInfo foundException = null; // First look for matches on the exception name for (ExceptionInfo exceptionInfo : throwsList) { if (exceptionInfo.getName().getText().equals( documentedClassInfo.getName().getText())) { found = true; foundException = exceptionInfo; break; } } // Now match on the exception type final ListIterator<ExceptionInfo> exceptionInfoIt = throwsList.listIterator(); while (!found && exceptionInfoIt.hasNext()) { final ExceptionInfo exceptionInfo = exceptionInfoIt.next(); if (documentedClassInfo.getClazz() == exceptionInfo.getClazz()) { found = true; foundException = exceptionInfo; } else if (allowThrowsTagsForSubclasses) { found = isSubclass(documentedClassInfo.getClazz(), exceptionInfo.getClazz()); } } if (foundException != null) { foundException.setFound(); foundThrows.add(documentedClassInfo.getName().getText()); } return found; } /** Stores useful information about declared exception. */ private static class ExceptionInfo { /** Class information associated with this exception. */ private final AbstractClassInfo classInfo; /** Does the exception have throws tag associated with. */ private boolean found; /** * Creates new instance for {@code FullIdent}. * * @param classInfo class info */ ExceptionInfo(AbstractClassInfo classInfo) { this.classInfo = classInfo; } /** Mark that the exception has associated throws tag. */ private void setFound() { found = true; } /** * Checks that the exception has throws tag associated with it. * @return whether the exception has throws tag associated with */ private boolean isFound() { return found; } /** * Gets exception name. * @return exception's name */ private Token getName() { return classInfo.getName(); } /** * Gets exception class. * @return class for this exception */ private Class<?> getClazz() { return classInfo.getClazz(); } } }