//////////////////////////////////////////////////////////////////////////////// // 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.github.sevntu.checkstyle.checks.design; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import antlr.collections.AST; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility; /** * Prevents using wildcards as return type of methods. * <p> * <i>Joshua Bloch, "Effective Java (2nd edition)" Item 28: page 137 :</i> * </p> * <p> * "Do not use wildcard types as return types. Rather than providing * additional flexibility for your users, * it would force them to use wildcard types in client code. Properly used, * wildcard types are nearly invisible to users of a class. They cause methods * to accept the parameters they should accept and reject those they should * reject. If the user of a class has to think about wildcard types, there is * probably something wrong with the class’s API." * </p> * @author <a href='mailto:barataliba@gmail.com'>Baratali Izmailov</a> */ public class ForbidWildcardAsReturnTypeCheck extends AbstractCheck { /** * Key for error message. */ public static final String MSG_KEY = "forbid.wildcard.as.return.type"; /** * Token of 'extends' keyword in bounded wildcard. */ private static final int WILDCARD_EXTENDS_IDENT = TokenTypes.TYPE_UPPER_BOUNDS; /** * Token of 'super' keyword in bounded wildcard. */ private static final int WILDCARD_SUPER_IDENT = TokenTypes.TYPE_LOWER_BOUNDS; /** {@link Deprecated Deprecated} annotation name. */ private static final String DEPRECATED = "Deprecated"; /** Fully-qualified {@link Deprecated Deprecated} annotation name. */ private static final String FQ_DEPRECATED = "java.lang." + DEPRECATED; /** {@link Override Override} annotation name. */ private static final String OVERRIDE = "Override"; /** Fully-qualified {@link Override Override} annotation name. */ private static final String FQ_OVERRIDE = "java.lang." + OVERRIDE; /** * Empty array of DetailAST. */ private static final DetailAST[] EMPTY_DETAILAST_ARRAY = new DetailAST[0]; /** * Check methods with 'public' modifier. */ private boolean checkPublicMethods = true; /** * Check methods with 'protected' modifier. */ private boolean checkProtectedMethods = true; /** * Check methods with 'package' modifier. */ private boolean checkPackageMethods = true; /** * Check methods with 'private' modifier. */ private boolean checkPrivateMethods; /** * Check methods with @Override annotation. */ private boolean checkOverrideMethods; /** * Check methods with @Deprecated annotation. */ private boolean checkDeprecatedMethods; /** * Allow wildcard with 'super'. Example: "? super T" */ private boolean allowReturnWildcardWithSuper; /** * Allow wildcard with 'extends'. Example: "? extends T" */ private boolean allowReturnWildcardWithExtends; /** * Ignore regexp for return type class names. */ private Pattern returnTypeClassNamesIgnoreRegex = Pattern.compile( "^(Comparator|Comparable)$"); /** * Setter for checkPublicMethods. * @param checkPublicMethods New value for the field. */ public void setCheckPublicMethods(boolean checkPublicMethods) { this.checkPublicMethods = checkPublicMethods; } /** * Setter for checkProtectedMethods. * @param checkProtectedMethods New value for the field. */ public void setCheckProtectedMethods(boolean checkProtectedMethods) { this.checkProtectedMethods = checkProtectedMethods; } /** * Setter for checkPackageMethods. * @param checkPackageMethods New value for the field. */ public void setCheckPackageMethods(boolean checkPackageMethods) { this.checkPackageMethods = checkPackageMethods; } /** * Setter for checkPrivateMethods. * @param checkPrivateMethods New value for the field. */ public void setCheckPrivateMethods(boolean checkPrivateMethods) { this.checkPrivateMethods = checkPrivateMethods; } /** * Setter for checkOverrideMethods. * @param checkOverrideMethods New value for the field. */ public void setCheckOverrideMethods(boolean checkOverrideMethods) { this.checkOverrideMethods = checkOverrideMethods; } /** * Setter for checkDeprecatedMethods. * @param checkDeprecatedMethods New value for the field. */ public void setCheckDeprecatedMethods(boolean checkDeprecatedMethods) { this.checkDeprecatedMethods = checkDeprecatedMethods; } /** * Setter for allowReturnWildcardWithSuper. * @param allowReturnWildcardWithSuper New value for the field. */ public void setAllowReturnWildcardWithSuper(boolean allowReturnWildcardWithSuper) { this.allowReturnWildcardWithSuper = allowReturnWildcardWithSuper; } /** * Setter for allowReturnWildcardWithExtends. * @param allowReturnWildcardWithExtends New value for the field. */ public void setAllowReturnWildcardWithExtends(boolean allowReturnWildcardWithExtends) { this.allowReturnWildcardWithExtends = allowReturnWildcardWithExtends; } /** * Setter for returnTypeClassNamesIgnoreRegex. * @param returnTypeClassNamesIgnoreRegex New value for the field. */ public void setReturnTypeClassNamesIgnoreRegex(String returnTypeClassNamesIgnoreRegex) { this.returnTypeClassNamesIgnoreRegex = Pattern.compile( returnTypeClassNamesIgnoreRegex); } @Override public int[] getDefaultTokens() { return new int[] { TokenTypes.METHOD_DEF, }; } @Override public void visitToken(DetailAST methodDefAst) { final String methodScope = getVisibilityScope(methodDefAst); if (((checkPublicMethods && "public".equals(methodScope)) || (checkPrivateMethods && "private".equals(methodScope)) || (checkProtectedMethods && "protected".equals(methodScope)) || (checkPackageMethods && "package".equals(methodScope))) && (checkOverrideMethods || (!AnnotationUtility.containsAnnotation(methodDefAst, OVERRIDE) && !AnnotationUtility.containsAnnotation(methodDefAst, FQ_OVERRIDE))) && (checkDeprecatedMethods || (!AnnotationUtility.containsAnnotation(methodDefAst, DEPRECATED) && !AnnotationUtility.containsAnnotation(methodDefAst, FQ_DEPRECATED)))) { final List<DetailAST> wildcardTypeArguments = getWildcardArgumentsAsMethodReturnType(methodDefAst); if (!wildcardTypeArguments.isEmpty() && !isIgnoreCase(methodDefAst, wildcardTypeArguments)) { log(methodDefAst.getLineNo(), MSG_KEY); } } } /** * Returns the visibility scope of method. * @param methodDefAst DetailAST of method definition. * @return one of "public", "private", "protected", "package" */ private static String getVisibilityScope(DetailAST methodDefAst) { String result = "package"; if (isInsideInterfaceDefinition(methodDefAst)) { result = "public"; } else { final String[] visibilityScopeModifiers = {"public", "private", "protected", }; final Set<String> methodModifiers = getModifiers(methodDefAst); for (final String modifier : visibilityScopeModifiers) { if (methodModifiers.contains(modifier)) { result = modifier; break; } } } return result; } /** * Verify that method definition is inside interface definition. * @param methodDefAst DetailAST of method definition. * @return true if method definition is inside interface definition. */ private static boolean isInsideInterfaceDefinition(DetailAST methodDefAst) { boolean result = false; final DetailAST objBlock = methodDefAst.getParent(); final DetailAST interfaceDef = objBlock.getParent(); if (interfaceDef.getType() == TokenTypes.INTERFACE_DEF) { result = true; } return result; } /** * Returns the set of modifier Strings for a METHOD_DEF AST. * @param methodDefAst AST for a method definition * @return the set of modifier Strings for aMethodDefAST */ private static Set<String> getModifiers(DetailAST methodDefAst) { final AST modifiersAst = methodDefAst.getFirstChild(); final Set<String> modifiersSet = new HashSet<>(); AST modifierAst = modifiersAst.getFirstChild(); while (modifierAst != null) { modifiersSet.add(modifierAst.getText()); modifierAst = modifierAst.getNextSibling(); } return modifiersSet; } /** * Get identifier of aAST. * @param ast * DetailAST instance * @return identifier of aAST, null if AST does not have identifier. */ private static String getIdentifier(final DetailAST ast) { final DetailAST identifier = ast.findFirstToken(TokenTypes.IDENT); final String result = identifier.getText(); return result; } /** * Receive list of arguments(AST nodes) which have wildcard. * @param methodDefAst * DetailAST of method definition. * @return list of arguments which have wildcard. */ private static List<DetailAST> getWildcardArgumentsAsMethodReturnType(DetailAST methodDefAst) { final List<DetailAST> result = new LinkedList<>(); final DetailAST methodTypeAst = methodDefAst.findFirstToken(TokenTypes.TYPE); final DetailAST[] methodTypeArgumentTokens = getGenericTypeArguments(methodTypeAst); for (DetailAST typeArgumentAst: methodTypeArgumentTokens) { if (hasChildToken(typeArgumentAst, TokenTypes.WILDCARD_TYPE)) { result.add(typeArgumentAst); } } return result; } /** * Get all type arguments of TypeAST. * @param typeAst * DetailAST of type definition. * @return array of type arguments. */ private static DetailAST[] getGenericTypeArguments(DetailAST typeAst) { DetailAST[] result = EMPTY_DETAILAST_ARRAY; if (hasChildToken(typeAst, TokenTypes.TYPE_ARGUMENTS)) { final DetailAST typeArguments = typeAst .findFirstToken(TokenTypes.TYPE_ARGUMENTS); final int argumentsCount = typeArguments .getChildCount(TokenTypes.TYPE_ARGUMENT); result = new DetailAST[argumentsCount]; DetailAST firstTypeArgument = typeArguments .findFirstToken(TokenTypes.TYPE_ARGUMENT); int counter = 0; while (firstTypeArgument != null) { if (firstTypeArgument.getType() == TokenTypes.TYPE_ARGUMENT) { result[counter] = firstTypeArgument; counter++; } firstTypeArgument = firstTypeArgument.getNextSibling(); } } return result; } /** * Verify that aAST has token of aTokenType type. * @param ast * DetailAST instance. * @param tokenType * one of TokenTypes * @return true if aAST has token of given type, or false otherwise. */ private static boolean hasChildToken(DetailAST ast, int tokenType) { return ast.findFirstToken(tokenType) != null; } /** * Verify that method with wildcards as return type is allowed by current * check configuration. * @param methodDefAst DetailAST of method definition. * @param wildcardTypeArguments list of wildcard type arguments. * @return true if method is allowed by current check configuration. */ private boolean isIgnoreCase(DetailAST methodDefAst, List<DetailAST> wildcardTypeArguments) { boolean result = false; if (matchesIgnoreClassNames(methodDefAst)) { result = true; } else { final boolean hasExtendsWildcardAsReturnType = hasBoundedWildcardAsReturnType(wildcardTypeArguments, WILDCARD_EXTENDS_IDENT); final boolean hasSuperWildcardAsReturnType = hasBoundedWildcardAsReturnType(wildcardTypeArguments, WILDCARD_SUPER_IDENT); final boolean hasOnlyExtendsWildcardAsReturnType = hasExtendsWildcardAsReturnType && !hasSuperWildcardAsReturnType; final boolean hasOnlySuperWildcardAsReturnType = hasSuperWildcardAsReturnType && !hasExtendsWildcardAsReturnType; final boolean hasBoundedWildcardAsReturnType = hasExtendsWildcardAsReturnType || hasSuperWildcardAsReturnType; final boolean isAllowedBoundedWildcards = allowReturnWildcardWithExtends && allowReturnWildcardWithSuper; result = (isAllowedBoundedWildcards && hasBoundedWildcardAsReturnType) || (allowReturnWildcardWithExtends && hasOnlyExtendsWildcardAsReturnType) || (allowReturnWildcardWithSuper && hasOnlySuperWildcardAsReturnType); } return result; } /** * Verify that method's return type name matches ignore regexp. * @param methodDefAst DetailAST of method. * @return true if aMethodDefAST's name matches ignore regexp. * false otherwise. */ private boolean matchesIgnoreClassNames(DetailAST methodDefAst) { final DetailAST methodTypeAst = methodDefAst.findFirstToken(TokenTypes.TYPE); final String typeIdentifier = getIdentifier(methodTypeAst); return returnTypeClassNamesIgnoreRegex .matcher(typeIdentifier).matches(); } /** * Verify that method has bounded wildcard in type arguments list. * @param typeArgumentsList list of type arguments. * @param boundedWildcardType type of bounded wildcard. * @return true if aTypeArgumentsList contains bounded wildcard. */ private static boolean hasBoundedWildcardAsReturnType( final List<DetailAST> typeArgumentsList, int boundedWildcardType) { boolean result = false; for (DetailAST typeArgumentAst: typeArgumentsList) { if (hasChildToken(typeArgumentAst, boundedWildcardType)) { result = true; break; } } return result; } }