001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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; 027import java.util.stream.Stream; 028 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 an 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 practise 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 */ 094public class DesignForExtensionCheck extends AbstractCheck { 095 096 /** 097 * A key is pointing to the warning message text in "messages.properties" 098 * file. 099 */ 100 public static final String MSG_KEY = "design.forExtension"; 101 102 /** 103 * A set of annotations which allow the check to skip the method from validation. 104 */ 105 private Set<String> ignoredAnnotations = Stream.of("Test", "Before", "After", "BeforeClass", 106 "AfterClass").collect(Collectors.toSet()); 107 108 /** 109 * Sets annotations which allow the check to skip the method from validation. 110 * @param ignoredAnnotations method annotations. 111 */ 112 public void setIgnoredAnnotations(String... ignoredAnnotations) { 113 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet()); 114 } 115 116 @Override 117 public int[] getDefaultTokens() { 118 return getAcceptableTokens(); 119 } 120 121 @Override 122 public int[] getAcceptableTokens() { 123 // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check 124 // subscribes to CLASS_DEF token it will become stateful, since we need to have additional 125 // stack to hold CLASS_DEF tokens. 126 return new int[] {TokenTypes.METHOD_DEF}; 127 } 128 129 @Override 130 public int[] getRequiredTokens() { 131 return getAcceptableTokens(); 132 } 133 134 @Override 135 public boolean isCommentNodesRequired() { 136 return true; 137 } 138 139 @Override 140 public void visitToken(DetailAST ast) { 141 if (!hasJavadocComment(ast) 142 && (isNativeMethod(ast) 143 || !hasEmptyImplementation(ast)) 144 && canBeOverridden(ast) 145 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) { 146 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 boolean hasJavadocComment(DetailAST methodDef) { 162 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 163 return modifiers.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN); 164 } 165 166 /** 167 * Checks whether a methods is native. 168 * @param ast method definition token. 169 * @return true if a methods is native. 170 */ 171 private boolean isNativeMethod(DetailAST ast) { 172 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); 173 return mods.branchContains(TokenTypes.LITERAL_NATIVE); 174 } 175 176 /** 177 * Checks whether a method has only comments in the body (has an empty implementation). 178 * Method is OK if its implementation is empty. 179 * @param ast method definition token. 180 * @return true if a method has only comments in the body. 181 */ 182 private static boolean hasEmptyImplementation(DetailAST ast) { 183 boolean hasEmptyBody = true; 184 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST); 185 if (methodImplOpenBrace != null) { 186 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild(); 187 final Predicate<DetailAST> predicate = currentNode -> 188 currentNode != null 189 && currentNode != methodImplCloseBrace 190 && currentNode.getLineNo() <= methodImplCloseBrace.getLineNo() 191 && !TokenUtils.isCommentType(currentNode.getType()); 192 final Optional<DetailAST> methodBody = 193 TokenUtils.findFirstTokenByPredicate(methodImplOpenBrace, predicate); 194 if (methodBody.isPresent()) { 195 hasEmptyBody = false; 196 } 197 } 198 return hasEmptyBody; 199 } 200 201 /** 202 * Checks whether a method can be overridden. 203 * Method can be overridden if it is not private, abstract, final or static. 204 * Note that the check has nothing to do for interfaces. 205 * @param methodDef method definition token. 206 * @return true if a method can be overridden in a subclass. 207 */ 208 private boolean canBeOverridden(DetailAST methodDef) { 209 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 210 return ScopeUtils.getSurroundingScope(methodDef).isIn(Scope.PROTECTED) 211 && !ScopeUtils.isInInterfaceOrAnnotationBlock(methodDef) 212 && !modifiers.branchContains(TokenTypes.LITERAL_PRIVATE) 213 && !modifiers.branchContains(TokenTypes.ABSTRACT) 214 && !modifiers.branchContains(TokenTypes.FINAL) 215 && !modifiers.branchContains(TokenTypes.LITERAL_STATIC); 216 } 217 218 /** 219 * Checks whether a method has any of ignored annotations. 220 * @param methodDef method definition token. 221 * @param annotations a set of ignored annotations. 222 * @return true if a method has any of ignored annotations. 223 */ 224 private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) { 225 return annotations.stream().filter(annotation -> hasAnnotation(methodDef, annotation)) 226 .findAny().isPresent(); 227 } 228 229 /** 230 * Check if a method has specific annotation. 231 * @param methodDef method definition token. 232 * @param annotationName annotation name. 233 * @return true, if a method has a specific annotation. 234 */ 235 private static boolean hasAnnotation(DetailAST methodDef, String annotationName) { 236 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 237 boolean containsAnnotation = false; 238 if (modifiers.branchContains(TokenTypes.ANNOTATION)) { 239 final Optional<DetailAST> annotation = TokenUtils.findFirstTokenByPredicate(modifiers, 240 currentToken -> currentToken != null 241 && currentToken.getType() == TokenTypes.ANNOTATION 242 && annotationName.equals(getAnnotationName(currentToken))); 243 if (annotation.isPresent()) { 244 containsAnnotation = true; 245 } 246 } 247 return containsAnnotation; 248 } 249 250 /** 251 * Gets the name of the annotation. 252 * @param annotation to get name of. 253 * @return the name of the annotation. 254 */ 255 private static String getAnnotationName(DetailAST annotation) { 256 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 257 final String name; 258 if (dotAst == null) { 259 name = annotation.findFirstToken(TokenTypes.IDENT).getText(); 260 } 261 else { 262 name = dotAst.findFirstToken(TokenTypes.IDENT).getText(); 263 } 264 return name; 265 } 266 267 /** 268 * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node. 269 * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node. 270 * @param ast the start node for searching. 271 * @return the CLASS_DEF or ENUM_DEF token. 272 */ 273 private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) { 274 DetailAST searchAST = ast; 275 while (searchAST.getType() != TokenTypes.CLASS_DEF 276 && searchAST.getType() != TokenTypes.ENUM_DEF) { 277 searchAST = searchAST.getParent(); 278 } 279 return searchAST; 280 } 281 282 /** 283 * Checks if the given class (given CLASS_DEF node) can be subclassed. 284 * @param classDef class definition token. 285 * @return true if the containing class can be subclassed. 286 */ 287 private static boolean canBeSubclassed(DetailAST classDef) { 288 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 289 return classDef.getType() != TokenTypes.ENUM_DEF 290 && !modifiers.branchContains(TokenTypes.FINAL) 291 && hasDefaultOrExplicitNonPrivateCtor(classDef); 292 } 293 294 /** 295 * Checks whether a class has default or explicit non-private constructor. 296 * @param classDef class ast token. 297 * @return true if a class has default or explicit non-private constructor. 298 */ 299 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) { 300 // check if subclassing is prevented by having only private ctors 301 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK); 302 303 boolean hasDefaultConstructor = true; 304 boolean hasExplicitNonPrivateCtor = false; 305 306 DetailAST candidate = objBlock.getFirstChild(); 307 308 while (candidate != null) { 309 if (candidate.getType() == TokenTypes.CTOR_DEF) { 310 hasDefaultConstructor = false; 311 312 final DetailAST ctorMods = 313 candidate.findFirstToken(TokenTypes.MODIFIERS); 314 if (!ctorMods.branchContains(TokenTypes.LITERAL_PRIVATE)) { 315 hasExplicitNonPrivateCtor = true; 316 break; 317 } 318 } 319 candidate = candidate.getNextSibling(); 320 } 321 322 return hasDefaultConstructor || hasExplicitNonPrivateCtor; 323 } 324}