001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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.design; 021 022import java.util.Arrays; 023import java.util.Optional; 024import java.util.Set; 025import java.util.function.Predicate; 026import java.util.stream.Collectors; 027 028import com.puppycrawl.tools.checkstyle.StatelessCheck; 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.Scope; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 034import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 035 036/** 037 * The check finds classes that are designed for extension (subclass creation). 038 * 039 * <p> 040 * Nothing wrong could be with founded classes. 041 * This check makes sense only for library projects (not application projects) 042 * which care of ideal OOP-design to make sure that class works in all cases even misusage. 043 * Even in library projects this check most likely will find classes that are designed for extension 044 * by somebody. User needs to use suppressions extensively to got a benefit from this check, 045 * and keep in suppressions all confirmed/known classes that are deigned for inheritance 046 * intentionally to let the check catch only new classes, and bring this to team/user attention. 047 * </p> 048 * 049 * <p> 050 * ATTENTION: Only user can decide whether a class is designed for extension or not. 051 * The check just shows all classes which are possibly designed for extension. 052 * If smth inappropriate is found please use suppression. 053 * </p> 054 * 055 * <p> 056 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment 057 * (a good practice is to explain its self-use of overridable methods) the check will not 058 * rise a violation. The violation can also be skipped if the method which can be overridden 059 * in a subclass has one or more annotations that are specified in ignoredAnnotations 060 * option. Note, that by default @Override annotation is not included in the 061 * ignoredAnnotations set as in a subclass the method which has the annotation can also be 062 * overridden in its subclass. 063 * </p> 064 * 065 * <p> 066 * More specifically, the check enforces a programming style where superclasses provide empty 067 * "hooks" that can be implemented by subclasses. 068 * </p> 069 * 070 * <p> 071 * The check finds classes that have overridable methods (public or protected methods 072 * that are non-static, not-final, non-abstract) and have non-empty implementation. 073 * </p> 074 * 075 * <p> 076 * This protects superclasses against being broken by subclasses. The downside is that subclasses 077 * are limited in their flexibility, in particular, they cannot prevent execution of code in the 078 * superclass, but that also means that subclasses cannot forget to call their super method. 079 * </p> 080 * 081 * <p> 082 * The check has the following options: 083 * </p> 084 * <ul> 085 * <li> 086 * ignoredAnnotations - annotations which allow the check to skip the method from validation. 087 * Default value is <b>Test, Before, After, BeforeClass, AfterClass</b>. 088 * </li> 089 * </ul> 090 * 091 */ 092@StatelessCheck 093public class DesignForExtensionCheck extends AbstractCheck { 094 095 /** 096 * A key is pointing to the warning message text in "messages.properties" 097 * file. 098 */ 099 public static final String MSG_KEY = "design.forExtension"; 100 101 /** 102 * A set of annotations which allow the check to skip the method from validation. 103 */ 104 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After", 105 "BeforeClass", "AfterClass", }).collect(Collectors.toSet()); 106 107 /** 108 * Sets annotations which allow the check to skip the method from validation. 109 * @param ignoredAnnotations method annotations. 110 */ 111 public void setIgnoredAnnotations(String... ignoredAnnotations) { 112 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet()); 113 } 114 115 @Override 116 public int[] getDefaultTokens() { 117 return getRequiredTokens(); 118 } 119 120 @Override 121 public int[] getAcceptableTokens() { 122 return getRequiredTokens(); 123 } 124 125 @Override 126 public int[] getRequiredTokens() { 127 // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check 128 // subscribes to CLASS_DEF token it will become stateful, since we need to have additional 129 // stack to hold CLASS_DEF tokens. 130 return new int[] {TokenTypes.METHOD_DEF}; 131 } 132 133 @Override 134 public boolean isCommentNodesRequired() { 135 return true; 136 } 137 138 @Override 139 public void visitToken(DetailAST ast) { 140 if (!hasJavadocComment(ast) 141 && canBeOverridden(ast) 142 && (isNativeMethod(ast) 143 || !hasEmptyImplementation(ast)) 144 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) { 145 final DetailAST classDef = getNearestClassOrEnumDefinition(ast); 146 if (canBeSubclassed(classDef)) { 147 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 148 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText(); 149 log(ast, MSG_KEY, className, methodName); 150 } 151 } 152 } 153 154 /** 155 * Checks whether a method has a javadoc comment. 156 * @param methodDef method definition token. 157 * @return true if a method has a javadoc comment. 158 */ 159 private static boolean hasJavadocComment(DetailAST methodDef) { 160 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS) 161 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE); 162 } 163 164 /** 165 * Checks whether a token has a javadoc comment. 166 * 167 * @param methodDef method definition token. 168 * @param tokenType token type. 169 * @return true if a token has a javadoc comment. 170 */ 171 private static boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) { 172 final DetailAST token = methodDef.findFirstToken(tokenType); 173 return token.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN); 174 } 175 176 /** 177 * Checks whether a methods is native. 178 * @param ast method definition token. 179 * @return true if a methods is native. 180 */ 181 private static boolean isNativeMethod(DetailAST ast) { 182 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); 183 return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null; 184 } 185 186 /** 187 * Checks whether a method has only comments in the body (has an empty implementation). 188 * Method is OK if its implementation is empty. 189 * @param ast method definition token. 190 * @return true if a method has only comments in the body. 191 */ 192 private static boolean hasEmptyImplementation(DetailAST ast) { 193 boolean hasEmptyBody = true; 194 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST); 195 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild(); 196 final Predicate<DetailAST> predicate = currentNode -> { 197 return currentNode != methodImplCloseBrace 198 && !TokenUtil.isCommentType(currentNode.getType()); 199 }; 200 final Optional<DetailAST> methodBody = 201 TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate); 202 if (methodBody.isPresent()) { 203 hasEmptyBody = false; 204 } 205 return hasEmptyBody; 206 } 207 208 /** 209 * Checks whether a method can be overridden. 210 * Method can be overridden if it is not private, abstract, final or static. 211 * Note that the check has nothing to do for interfaces. 212 * @param methodDef method definition token. 213 * @return true if a method can be overridden in a subclass. 214 */ 215 private static boolean canBeOverridden(DetailAST methodDef) { 216 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 217 return ScopeUtil.getSurroundingScope(methodDef).isIn(Scope.PROTECTED) 218 && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef) 219 && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null 220 && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null 221 && modifiers.findFirstToken(TokenTypes.FINAL) == null 222 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null; 223 } 224 225 /** 226 * Checks whether a method has any of ignored annotations. 227 * @param methodDef method definition token. 228 * @param annotations a set of ignored annotations. 229 * @return true if a method has any of ignored annotations. 230 */ 231 private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) { 232 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 233 boolean hasIgnoredAnnotation = false; 234 if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) { 235 final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers, 236 currentToken -> { 237 return currentToken.getType() == TokenTypes.ANNOTATION 238 && annotations.contains(getAnnotationName(currentToken)); 239 }); 240 if (annotation.isPresent()) { 241 hasIgnoredAnnotation = true; 242 } 243 } 244 return hasIgnoredAnnotation; 245 } 246 247 /** 248 * Gets the name of the annotation. 249 * @param annotation to get name of. 250 * @return the name of the annotation. 251 */ 252 private static String getAnnotationName(DetailAST annotation) { 253 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 254 final String name; 255 if (dotAst == null) { 256 name = annotation.findFirstToken(TokenTypes.IDENT).getText(); 257 } 258 else { 259 name = dotAst.findFirstToken(TokenTypes.IDENT).getText(); 260 } 261 return name; 262 } 263 264 /** 265 * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node. 266 * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node. 267 * @param ast the start node for searching. 268 * @return the CLASS_DEF or ENUM_DEF token. 269 */ 270 private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) { 271 DetailAST searchAST = ast; 272 while (searchAST.getType() != TokenTypes.CLASS_DEF 273 && searchAST.getType() != TokenTypes.ENUM_DEF) { 274 searchAST = searchAST.getParent(); 275 } 276 return searchAST; 277 } 278 279 /** 280 * Checks if the given class (given CLASS_DEF node) can be subclassed. 281 * @param classDef class definition token. 282 * @return true if the containing class can be subclassed. 283 */ 284 private static boolean canBeSubclassed(DetailAST classDef) { 285 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 286 return classDef.getType() != TokenTypes.ENUM_DEF 287 && modifiers.findFirstToken(TokenTypes.FINAL) == null 288 && hasDefaultOrExplicitNonPrivateCtor(classDef); 289 } 290 291 /** 292 * Checks whether a class has default or explicit non-private constructor. 293 * @param classDef class ast token. 294 * @return true if a class has default or explicit non-private constructor. 295 */ 296 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) { 297 // check if subclassing is prevented by having only private ctors 298 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK); 299 300 boolean hasDefaultConstructor = true; 301 boolean hasExplicitNonPrivateCtor = false; 302 303 DetailAST candidate = objBlock.getFirstChild(); 304 305 while (candidate != null) { 306 if (candidate.getType() == TokenTypes.CTOR_DEF) { 307 hasDefaultConstructor = false; 308 309 final DetailAST ctorMods = 310 candidate.findFirstToken(TokenTypes.MODIFIERS); 311 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 312 hasExplicitNonPrivateCtor = true; 313 break; 314 } 315 } 316 candidate = candidate.getNextSibling(); 317 } 318 319 return hasDefaultConstructor || hasExplicitNonPrivateCtor; 320 } 321 322}