Coverage Report - com.puppycrawl.tools.checkstyle.checks.design.DesignForExtensionCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
DesignForExtensionCheck
100%
81/81
100%
66/66
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2018 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.checks.design;
 21  
 
 22  
 import java.util.Arrays;
 23  
 import java.util.Optional;
 24  
 import java.util.Set;
 25  
 import java.util.function.Predicate;
 26  
 import java.util.stream.Collectors;
 27  
 
 28  
 import com.puppycrawl.tools.checkstyle.StatelessCheck;
 29  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 30  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 31  
 import com.puppycrawl.tools.checkstyle.api.Scope;
 32  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 33  
 import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
 34  
 import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
 35  
 
 36  
 /**
 37  
  * The check finds classes that are designed for extension (subclass creation).
 38  
  *
 39  
  * <p>
 40  
  * Nothing wrong could be with founded classes.
 41  
  * This check makes sense only for library projects (not application projects)
 42  
  * which care of ideal OOP-design to make sure that class works in all cases even misusage.
 43  
  * Even in library projects this check most likely will find classes that are designed for extension
 44  
  * by somebody. User needs to use suppressions extensively to got a benefit from this check,
 45  
  * and keep in suppressions all confirmed/known classes that are deigned for inheritance
 46  
  * intentionally to let the check catch only new classes, and bring this to team/user attention.
 47  
  * </p>
 48  
  *
 49  
  * <p>
 50  
  * ATTENTION: Only user can decide whether a class is designed for extension or not.
 51  
  * The check just shows all classes which are possibly designed for extension.
 52  
  * If smth inappropriate is found please use suppression.
 53  
  * </p>
 54  
  *
 55  
  * <p>
 56  
  * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
 57  
  * (a good practice is to explain its self-use of overridable methods) the check will not
 58  
  * rise a violation. The violation can also be skipped if the method which can be overridden
 59  
  * in a subclass has one or more annotations that are specified in ignoredAnnotations
 60  
  * option. Note, that by default @Override annotation is not included in the
 61  
  * ignoredAnnotations set as in a subclass the method which has the annotation can also be
 62  
  * overridden in its subclass.
 63  
  * </p>
 64  
  *
 65  
  * <p>
 66  
  * More specifically, the check enforces a programming style where superclasses provide empty
 67  
  * "hooks" that can be implemented by subclasses.
 68  
  * </p>
 69  
  *
 70  
  * <p>
 71  
  * The check finds classes that have overridable methods (public or protected methods
 72  
  * that are non-static, not-final, non-abstract) and have non-empty implementation.
 73  
  * </p>
 74  
  *
 75  
  * <p>
 76  
  * This protects superclasses against being broken by subclasses. The downside is that subclasses
 77  
  * are limited in their flexibility, in particular, they cannot prevent execution of code in the
 78  
  * superclass, but that also means that subclasses cannot forget to call their super method.
 79  
  * </p>
 80  
  *
 81  
  * <p>
 82  
  * The check has the following options:
 83  
  * </p>
 84  
  * <ul>
 85  
  * <li>
 86  
  * ignoredAnnotations - annotations which allow the check to skip the method from validation.
 87  
  * Default value is <b>Test, Before, After, BeforeClass, AfterClass</b>.
 88  
  * </li>
 89  
  * </ul>
 90  
  *
 91  
  * @author lkuehne
 92  
  * @author Andrei Selkin
 93  
  */
 94  
 @StatelessCheck
 95  15
 public class DesignForExtensionCheck extends AbstractCheck {
 96  
 
 97  
     /**
 98  
      * A key is pointing to the warning message text in "messages.properties"
 99  
      * file.
 100  
      */
 101  
     public static final String MSG_KEY = "design.forExtension";
 102  
 
 103  
     /**
 104  
      * A set of annotations which allow the check to skip the method from validation.
 105  
      */
 106  30
     private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
 107  15
         "BeforeClass", "AfterClass", }).collect(Collectors.toSet());
 108  
 
 109  
     /**
 110  
      * Sets annotations which allow the check to skip the method from validation.
 111  
      * @param ignoredAnnotations method annotations.
 112  
      */
 113  
     public void setIgnoredAnnotations(String... ignoredAnnotations) {
 114  4
         this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet());
 115  4
     }
 116  
 
 117  
     @Override
 118  
     public int[] getDefaultTokens() {
 119  20
         return getRequiredTokens();
 120  
     }
 121  
 
 122  
     @Override
 123  
     public int[] getAcceptableTokens() {
 124  5
         return getRequiredTokens();
 125  
     }
 126  
 
 127  
     @Override
 128  
     public int[] getRequiredTokens() {
 129  
         // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
 130  
         // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
 131  
         // stack to hold CLASS_DEF tokens.
 132  46
         return new int[] {TokenTypes.METHOD_DEF};
 133  
     }
 134  
 
 135  
     @Override
 136  
     public boolean isCommentNodesRequired() {
 137  16
         return true;
 138  
     }
 139  
 
 140  
     @Override
 141  
     public void visitToken(DetailAST ast) {
 142  114
         if (!hasJavadocComment(ast)
 143  70
                 && canBeOverridden(ast)
 144  53
                 && (isNativeMethod(ast)
 145  51
                     || !hasEmptyImplementation(ast))
 146  41
                 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) {
 147  25
             final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
 148  25
             if (canBeSubclassed(classDef)) {
 149  20
                 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
 150  20
                 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
 151  20
                 log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY, className, methodName);
 152  
             }
 153  
         }
 154  114
     }
 155  
 
 156  
     /**
 157  
      * Checks whether a method has a javadoc comment.
 158  
      * @param methodDef method definition token.
 159  
      * @return true if a method has a javadoc comment.
 160  
      */
 161  
     private static boolean hasJavadocComment(DetailAST methodDef) {
 162  228
         return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
 163  72
                 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
 164  
     }
 165  
 
 166  
     /**
 167  
      * Checks whether a token has a javadoc comment.
 168  
      *
 169  
      * @param methodDef method definition token.
 170  
      * @param tokenType token type.
 171  
      * @return true if a token has a javadoc comment.
 172  
      */
 173  
     private static boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) {
 174  186
         final DetailAST token = methodDef.findFirstToken(tokenType);
 175  186
         return token.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN);
 176  
     }
 177  
 
 178  
     /**
 179  
      * Checks whether a methods is native.
 180  
      * @param ast method definition token.
 181  
      * @return true if a methods is native.
 182  
      */
 183  
     private static boolean isNativeMethod(DetailAST ast) {
 184  53
         final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
 185  53
         return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
 186  
     }
 187  
 
 188  
     /**
 189  
      * Checks whether a method has only comments in the body (has an empty implementation).
 190  
      * Method is OK if its implementation is empty.
 191  
      * @param ast method definition token.
 192  
      * @return true if a method has only comments in the body.
 193  
      */
 194  
     private static boolean hasEmptyImplementation(DetailAST ast) {
 195  51
         boolean hasEmptyBody = true;
 196  51
         final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
 197  51
         final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
 198  51
         final Predicate<DetailAST> predicate = currentNode -> {
 199  138
             return currentNode != methodImplCloseBrace
 200  57
                 && !TokenUtils.isCommentType(currentNode.getType());
 201  
         };
 202  51
         final Optional<DetailAST> methodBody =
 203  51
             TokenUtils.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
 204  51
         if (methodBody.isPresent()) {
 205  39
             hasEmptyBody = false;
 206  
         }
 207  51
         return hasEmptyBody;
 208  
     }
 209  
 
 210  
     /**
 211  
      * Checks whether a method can be overridden.
 212  
      * Method can be overridden if it is not private, abstract, final or static.
 213  
      * Note that the check has nothing to do for interfaces.
 214  
      * @param methodDef method definition token.
 215  
      * @return true if a method can be overridden in a subclass.
 216  
      */
 217  
     private static boolean canBeOverridden(DetailAST methodDef) {
 218  70
         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
 219  140
         return ScopeUtils.getSurroundingScope(methodDef).isIn(Scope.PROTECTED)
 220  68
             && !ScopeUtils.isInInterfaceOrAnnotationBlock(methodDef)
 221  67
             && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
 222  64
             && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null
 223  62
             && modifiers.findFirstToken(TokenTypes.FINAL) == null
 224  56
             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null;
 225  
     }
 226  
 
 227  
     /**
 228  
      * Checks whether a method has any of ignored annotations.
 229  
      * @param methodDef method definition token.
 230  
      * @param annotations a set of ignored annotations.
 231  
      * @return true if a method has any of ignored annotations.
 232  
      */
 233  
     private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
 234  41
         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
 235  41
         boolean hasIgnoredAnnotation = false;
 236  41
         if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) {
 237  20
             final Optional<DetailAST> annotation = TokenUtils.findFirstTokenByPredicate(modifiers,
 238  
                 currentToken -> {
 239  62
                     return currentToken.getType() == TokenTypes.ANNOTATION
 240  22
                         && annotations.contains(getAnnotationName(currentToken));
 241  
                 });
 242  20
             if (annotation.isPresent()) {
 243  16
                 hasIgnoredAnnotation = true;
 244  
             }
 245  
         }
 246  41
         return hasIgnoredAnnotation;
 247  
     }
 248  
 
 249  
     /**
 250  
      * Gets the name of the annotation.
 251  
      * @param annotation to get name of.
 252  
      * @return the name of the annotation.
 253  
      */
 254  
     private static String getAnnotationName(DetailAST annotation) {
 255  22
         final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
 256  
         final String name;
 257  22
         if (dotAst == null) {
 258  20
             name = annotation.findFirstToken(TokenTypes.IDENT).getText();
 259  
         }
 260  
         else {
 261  2
             name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
 262  
         }
 263  22
         return name;
 264  
     }
 265  
 
 266  
     /**
 267  
      * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
 268  
      * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
 269  
      * @param ast the start node for searching.
 270  
      * @return the CLASS_DEF or ENUM_DEF token.
 271  
      */
 272  
     private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
 273  25
         DetailAST searchAST = ast;
 274  75
         while (searchAST.getType() != TokenTypes.CLASS_DEF
 275  51
                && searchAST.getType() != TokenTypes.ENUM_DEF) {
 276  50
             searchAST = searchAST.getParent();
 277  
         }
 278  25
         return searchAST;
 279  
     }
 280  
 
 281  
     /**
 282  
      * Checks if the given class (given CLASS_DEF node) can be subclassed.
 283  
      * @param classDef class definition token.
 284  
      * @return true if the containing class can be subclassed.
 285  
      */
 286  
     private static boolean canBeSubclassed(DetailAST classDef) {
 287  25
         final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
 288  50
         return classDef.getType() != TokenTypes.ENUM_DEF
 289  24
             && modifiers.findFirstToken(TokenTypes.FINAL) == null
 290  21
             && hasDefaultOrExplicitNonPrivateCtor(classDef);
 291  
     }
 292  
 
 293  
     /**
 294  
      * Checks whether a class has default or explicit non-private constructor.
 295  
      * @param classDef class ast token.
 296  
      * @return true if a class has default or explicit non-private constructor.
 297  
      */
 298  
     private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
 299  
         // check if subclassing is prevented by having only private ctors
 300  21
         final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
 301  
 
 302  21
         boolean hasDefaultConstructor = true;
 303  21
         boolean hasExplicitNonPrivateCtor = false;
 304  
 
 305  21
         DetailAST candidate = objBlock.getFirstChild();
 306  
 
 307  819
         while (candidate != null) {
 308  799
             if (candidate.getType() == TokenTypes.CTOR_DEF) {
 309  2
                 hasDefaultConstructor = false;
 310  
 
 311  2
                 final DetailAST ctorMods =
 312  2
                         candidate.findFirstToken(TokenTypes.MODIFIERS);
 313  2
                 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
 314  1
                     hasExplicitNonPrivateCtor = true;
 315  1
                     break;
 316  
                 }
 317  
             }
 318  798
             candidate = candidate.getNextSibling();
 319  
         }
 320  
 
 321  21
         return hasDefaultConstructor || hasExplicitNonPrivateCtor;
 322  
     }
 323  
 
 324  
 }