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.ScopeUtils; 034import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 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 * @author lkuehne 092 * @author Andrei Selkin 093 */ 094@StatelessCheck 095public class DesignForExtensionCheck extends AbstractCheck { 096 097 /** 098 * A key is pointing to the warning message text in "messages.properties" 099 * 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 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After", 107 "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 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet()); 115 } 116 117 @Override 118 public int[] getDefaultTokens() { 119 return getRequiredTokens(); 120 } 121 122 @Override 123 public int[] getAcceptableTokens() { 124 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 return new int[] {TokenTypes.METHOD_DEF}; 133 } 134 135 @Override 136 public boolean isCommentNodesRequired() { 137 return true; 138 } 139 140 @Override 141 public void visitToken(DetailAST ast) { 142 if (!hasJavadocComment(ast) 143 && canBeOverridden(ast) 144 && (isNativeMethod(ast) 145 || !hasEmptyImplementation(ast)) 146 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) { 147 final DetailAST classDef = getNearestClassOrEnumDefinition(ast); 148 if (canBeSubclassed(classDef)) { 149 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 150 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText(); 151 log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY, className, methodName); 152 } 153 } 154 } 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 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS) 163 || 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 final DetailAST token = methodDef.findFirstToken(tokenType); 175 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 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); 185 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 boolean hasEmptyBody = true; 196 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST); 197 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild(); 198 final Predicate<DetailAST> predicate = currentNode -> { 199 return currentNode != methodImplCloseBrace 200 && !TokenUtils.isCommentType(currentNode.getType()); 201 }; 202 final Optional<DetailAST> methodBody = 203 TokenUtils.findFirstTokenByPredicate(methodImplOpenBrace, predicate); 204 if (methodBody.isPresent()) { 205 hasEmptyBody = false; 206 } 207 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 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 219 return ScopeUtils.getSurroundingScope(methodDef).isIn(Scope.PROTECTED) 220 && !ScopeUtils.isInInterfaceOrAnnotationBlock(methodDef) 221 && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null 222 && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null 223 && modifiers.findFirstToken(TokenTypes.FINAL) == null 224 && 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 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 235 boolean hasIgnoredAnnotation = false; 236 if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) { 237 final Optional<DetailAST> annotation = TokenUtils.findFirstTokenByPredicate(modifiers, 238 currentToken -> { 239 return currentToken.getType() == TokenTypes.ANNOTATION 240 && annotations.contains(getAnnotationName(currentToken)); 241 }); 242 if (annotation.isPresent()) { 243 hasIgnoredAnnotation = true; 244 } 245 } 246 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 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 256 final String name; 257 if (dotAst == null) { 258 name = annotation.findFirstToken(TokenTypes.IDENT).getText(); 259 } 260 else { 261 name = dotAst.findFirstToken(TokenTypes.IDENT).getText(); 262 } 263 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 DetailAST searchAST = ast; 274 while (searchAST.getType() != TokenTypes.CLASS_DEF 275 && searchAST.getType() != TokenTypes.ENUM_DEF) { 276 searchAST = searchAST.getParent(); 277 } 278 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 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 288 return classDef.getType() != TokenTypes.ENUM_DEF 289 && modifiers.findFirstToken(TokenTypes.FINAL) == null 290 && 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 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK); 301 302 boolean hasDefaultConstructor = true; 303 boolean hasExplicitNonPrivateCtor = false; 304 305 DetailAST candidate = objBlock.getFirstChild(); 306 307 while (candidate != null) { 308 if (candidate.getType() == TokenTypes.CTOR_DEF) { 309 hasDefaultConstructor = false; 310 311 final DetailAST ctorMods = 312 candidate.findFirstToken(TokenTypes.MODIFIERS); 313 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 314 hasExplicitNonPrivateCtor = true; 315 break; 316 } 317 } 318 candidate = candidate.getNextSibling(); 319 } 320 321 return hasDefaultConstructor || hasExplicitNonPrivateCtor; 322 } 323 324}