1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2024 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.Objects; 24 import java.util.Optional; 25 import java.util.Set; 26 import java.util.function.Predicate; 27 import java.util.regex.Matcher; 28 import java.util.regex.Pattern; 29 import java.util.stream.Collectors; 30 31 import com.puppycrawl.tools.checkstyle.StatelessCheck; 32 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 33 import com.puppycrawl.tools.checkstyle.api.DetailAST; 34 import com.puppycrawl.tools.checkstyle.api.Scope; 35 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 36 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 37 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 38 import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 39 40 /** 41 * <p> 42 * Checks that classes are designed for extension (subclass creation). 43 * </p> 44 * <p> 45 * Nothing wrong could be with founded classes. 46 * This check makes sense only for library projects (not application projects) 47 * which care of ideal OOP-design to make sure that class works in all cases even misusage. 48 * Even in library projects this check most likely will find classes that are designed for extension 49 * by somebody. User needs to use suppressions extensively to got a benefit from this check, 50 * and keep in suppressions all confirmed/known classes that are deigned for inheritance 51 * intentionally to let the check catch only new classes, and bring this to team/user attention. 52 * </p> 53 * 54 * <p> 55 * ATTENTION: Only user can decide whether a class is designed for extension or not. 56 * The check just shows all classes which are possibly designed for extension. 57 * If smth inappropriate is found please use suppression. 58 * </p> 59 * 60 * <p> 61 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment 62 * (a good practice is to explain its self-use of overridable methods) the check will not 63 * rise a violation. The violation can also be skipped if the method which can be overridden 64 * in a subclass has one or more annotations that are specified in ignoredAnnotations 65 * option. Note, that by default @Override annotation is not included in the 66 * ignoredAnnotations set as in a subclass the method which has the annotation can also be 67 * overridden in its subclass. 68 * </p> 69 * <p> 70 * Problem is described at "Effective Java, 2nd Edition by Joshua Bloch" book, chapter 71 * "Item 17: Design and document for inheritance or else prohibit it". 72 * </p> 73 * <p> 74 * Some quotes from book: 75 * </p> 76 * <blockquote>The class must document its self-use of overridable methods. 77 * By convention, a method that invokes overridable methods contains a description 78 * of these invocations at the end of its documentation comment. The description 79 * begins with the phrase “This implementation.” 80 * </blockquote> 81 * <blockquote> 82 * The best solution to this problem is to prohibit subclassing in classes that 83 * are not designed and documented to be safely subclassed. 84 * </blockquote> 85 * <blockquote> 86 * If a concrete class does not implement a standard interface, then you may 87 * inconvenience some programmers by prohibiting inheritance. If you feel that you 88 * must allow inheritance from such a class, one reasonable approach is to ensure 89 * that the class never invokes any of its overridable methods and to document this 90 * fact. In other words, eliminate the class’s self-use of overridable methods entirely. 91 * In doing so, you’ll create a class that is reasonably safe to subclass. Overriding a 92 * method will never affect the behavior of any other method. 93 * </blockquote> 94 * <p> 95 * The check finds classes that have overridable methods (public or protected methods 96 * that are non-static, not-final, non-abstract) and have non-empty implementation. 97 * </p> 98 * <p> 99 * Rationale: This library design style protects superclasses against being broken 100 * by subclasses. The downside is that subclasses are limited in their flexibility, 101 * in particular they cannot prevent execution of code in the superclass, but that 102 * also means that subclasses cannot corrupt the state of the superclass by forgetting 103 * to call the superclass's method. 104 * </p> 105 * <p> 106 * More specifically, it enforces a programming style where superclasses provide 107 * empty "hooks" that can be implemented by subclasses. 108 * </p> 109 * <p> 110 * Example of code that cause violation as it is designed for extension: 111 * </p> 112 * <pre> 113 * public abstract class Plant { 114 * private String roots; 115 * private String trunk; 116 * 117 * protected void validate() { 118 * if (roots == null) throw new IllegalArgumentException("No roots!"); 119 * if (trunk == null) throw new IllegalArgumentException("No trunk!"); 120 * } 121 * 122 * public abstract void grow(); 123 * } 124 * 125 * public class Tree extends Plant { 126 * private List leaves; 127 * 128 * @Overrides 129 * protected void validate() { 130 * super.validate(); 131 * if (leaves == null) throw new IllegalArgumentException("No leaves!"); 132 * } 133 * 134 * public void grow() { 135 * validate(); 136 * } 137 * } 138 * </pre> 139 * <p> 140 * Example of code without violation: 141 * </p> 142 * <pre> 143 * public abstract class Plant { 144 * private String roots; 145 * private String trunk; 146 * 147 * private void validate() { 148 * if (roots == null) throw new IllegalArgumentException("No roots!"); 149 * if (trunk == null) throw new IllegalArgumentException("No trunk!"); 150 * validateEx(); 151 * } 152 * 153 * protected void validateEx() { } 154 * 155 * public abstract void grow(); 156 * } 157 * </pre> 158 * <ul> 159 * <li> 160 * Property {@code ignoredAnnotations} - Specify annotations which allow the check to 161 * skip the method from validation. 162 * Type is {@code java.lang.String[]}. 163 * Default value is {@code After, AfterClass, Before, BeforeClass, Test}. 164 * </li> 165 * <li> 166 * Property {@code requiredJavadocPhrase} - Specify the comment text pattern which qualifies a 167 * method as designed for extension. Supports multi-line regex. 168 * Type is {@code java.util.regex.Pattern}. 169 * Default value is {@code ".*"}. 170 * </li> 171 * </ul> 172 * <p> 173 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 174 * </p> 175 * <p> 176 * Violation Message Keys: 177 * </p> 178 * <ul> 179 * <li> 180 * {@code design.forExtension} 181 * </li> 182 * </ul> 183 * 184 * @since 3.1 185 */ 186 @StatelessCheck 187 public class DesignForExtensionCheck extends AbstractCheck { 188 189 /** 190 * A key is pointing to the warning message text in "messages.properties" 191 * file. 192 */ 193 public static final String MSG_KEY = "design.forExtension"; 194 195 /** 196 * Specify annotations which allow the check to skip the method from validation. 197 */ 198 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After", 199 "BeforeClass", "AfterClass", }).collect(Collectors.toUnmodifiableSet()); 200 201 /** 202 * Specify the comment text pattern which qualifies a method as designed for extension. 203 * Supports multi-line regex. 204 */ 205 private Pattern requiredJavadocPhrase = Pattern.compile(".*"); 206 207 /** 208 * Setter to specify annotations which allow the check to skip the method from validation. 209 * 210 * @param ignoredAnnotations method annotations. 211 * @since 7.2 212 */ 213 public void setIgnoredAnnotations(String... ignoredAnnotations) { 214 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations) 215 .collect(Collectors.toUnmodifiableSet()); 216 } 217 218 /** 219 * Setter to specify the comment text pattern which qualifies a 220 * method as designed for extension. Supports multi-line regex. 221 * 222 * @param requiredJavadocPhrase method annotations. 223 * @since 8.40 224 */ 225 public void setRequiredJavadocPhrase(Pattern requiredJavadocPhrase) { 226 this.requiredJavadocPhrase = requiredJavadocPhrase; 227 } 228 229 @Override 230 public int[] getDefaultTokens() { 231 return getRequiredTokens(); 232 } 233 234 @Override 235 public int[] getAcceptableTokens() { 236 return getRequiredTokens(); 237 } 238 239 @Override 240 public int[] getRequiredTokens() { 241 // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check 242 // subscribes to CLASS_DEF token it will become stateful, since we need to have additional 243 // stack to hold CLASS_DEF tokens. 244 return new int[] {TokenTypes.METHOD_DEF}; 245 } 246 247 @Override 248 public boolean isCommentNodesRequired() { 249 return true; 250 } 251 252 @Override 253 public void visitToken(DetailAST ast) { 254 if (!hasJavadocComment(ast) 255 && canBeOverridden(ast) 256 && (isNativeMethod(ast) 257 || !hasEmptyImplementation(ast)) 258 && !hasIgnoredAnnotation(ast, ignoredAnnotations) 259 && !ScopeUtil.isInRecordBlock(ast)) { 260 final DetailAST classDef = getNearestClassOrEnumDefinition(ast); 261 if (canBeSubclassed(classDef)) { 262 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 263 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText(); 264 log(ast, MSG_KEY, className, methodName); 265 } 266 } 267 } 268 269 /** 270 * Checks whether a method has a javadoc comment. 271 * 272 * @param methodDef method definition token. 273 * @return true if a method has a javadoc comment. 274 */ 275 private boolean hasJavadocComment(DetailAST methodDef) { 276 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS) 277 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE); 278 } 279 280 /** 281 * Checks whether a token has a javadoc comment. 282 * 283 * @param methodDef method definition token. 284 * @param tokenType token type. 285 * @return true if a token has a javadoc comment. 286 */ 287 private boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) { 288 final DetailAST token = methodDef.findFirstToken(tokenType); 289 return branchContainsJavadocComment(token); 290 } 291 292 /** 293 * Checks whether a javadoc comment exists under the token. 294 * 295 * @param token tree token. 296 * @return true if a javadoc comment exists under the token. 297 */ 298 private boolean branchContainsJavadocComment(DetailAST token) { 299 boolean result = false; 300 DetailAST curNode = token; 301 while (curNode != null) { 302 if (curNode.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 303 && JavadocUtil.isJavadocComment(curNode)) { 304 result = hasValidJavadocComment(curNode); 305 break; 306 } 307 308 DetailAST toVisit = curNode.getFirstChild(); 309 while (toVisit == null) { 310 if (curNode == token) { 311 break; 312 } 313 314 toVisit = curNode.getNextSibling(); 315 curNode = curNode.getParent(); 316 } 317 curNode = toVisit; 318 } 319 320 return result; 321 } 322 323 /** 324 * Checks whether a javadoc contains the specified comment pattern that denotes 325 * a method as designed for extension. 326 * 327 * @param detailAST the ast we are checking for possible extension 328 * @return true if the javadoc of this ast contains the required comment pattern 329 */ 330 private boolean hasValidJavadocComment(DetailAST detailAST) { 331 final String javadocString = 332 JavadocUtil.getBlockCommentContent(detailAST); 333 334 final Matcher requiredJavadocPhraseMatcher = 335 requiredJavadocPhrase.matcher(javadocString); 336 337 return requiredJavadocPhraseMatcher.find(); 338 } 339 340 /** 341 * Checks whether a method is native. 342 * 343 * @param ast method definition token. 344 * @return true if a methods is native. 345 */ 346 private static boolean isNativeMethod(DetailAST ast) { 347 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); 348 return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null; 349 } 350 351 /** 352 * Checks whether a method has only comments in the body (has an empty implementation). 353 * Method is OK if its implementation is empty. 354 * 355 * @param ast method definition token. 356 * @return true if a method has only comments in the body. 357 */ 358 private static boolean hasEmptyImplementation(DetailAST ast) { 359 boolean hasEmptyBody = true; 360 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST); 361 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild(); 362 final Predicate<DetailAST> predicate = currentNode -> { 363 return currentNode != methodImplCloseBrace 364 && !TokenUtil.isCommentType(currentNode.getType()); 365 }; 366 final Optional<DetailAST> methodBody = 367 TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate); 368 if (methodBody.isPresent()) { 369 hasEmptyBody = false; 370 } 371 return hasEmptyBody; 372 } 373 374 /** 375 * Checks whether a method can be overridden. 376 * Method can be overridden if it is not private, abstract, final or static. 377 * Note that the check has nothing to do for interfaces. 378 * 379 * @param methodDef method definition token. 380 * @return true if a method can be overridden in a subclass. 381 */ 382 private static boolean canBeOverridden(DetailAST methodDef) { 383 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 384 return ScopeUtil.getSurroundingScope(methodDef).isIn(Scope.PROTECTED) 385 && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef) 386 && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null 387 && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null 388 && modifiers.findFirstToken(TokenTypes.FINAL) == null 389 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null; 390 } 391 392 /** 393 * Checks whether a method has any of ignored annotations. 394 * 395 * @param methodDef method definition token. 396 * @param annotations a set of ignored annotations. 397 * @return true if a method has any of ignored annotations. 398 */ 399 private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) { 400 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 401 final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers, 402 currentToken -> { 403 return currentToken.getType() == TokenTypes.ANNOTATION 404 && annotations.contains(getAnnotationName(currentToken)); 405 }); 406 return annotation.isPresent(); 407 } 408 409 /** 410 * Gets the name of the annotation. 411 * 412 * @param annotation to get name of. 413 * @return the name of the annotation. 414 */ 415 private static String getAnnotationName(DetailAST annotation) { 416 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 417 final DetailAST parent = Objects.requireNonNullElse(dotAst, annotation); 418 return parent.findFirstToken(TokenTypes.IDENT).getText(); 419 } 420 421 /** 422 * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node. 423 * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node. 424 * 425 * @param ast the start node for searching. 426 * @return the CLASS_DEF or ENUM_DEF token. 427 */ 428 private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) { 429 DetailAST searchAST = ast; 430 while (searchAST.getType() != TokenTypes.CLASS_DEF 431 && searchAST.getType() != TokenTypes.ENUM_DEF) { 432 searchAST = searchAST.getParent(); 433 } 434 return searchAST; 435 } 436 437 /** 438 * Checks if the given class (given CLASS_DEF node) can be subclassed. 439 * 440 * @param classDef class definition token. 441 * @return true if the containing class can be subclassed. 442 */ 443 private static boolean canBeSubclassed(DetailAST classDef) { 444 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 445 return classDef.getType() != TokenTypes.ENUM_DEF 446 && modifiers.findFirstToken(TokenTypes.FINAL) == null 447 && hasDefaultOrExplicitNonPrivateCtor(classDef); 448 } 449 450 /** 451 * Checks whether a class has default or explicit non-private constructor. 452 * 453 * @param classDef class ast token. 454 * @return true if a class has default or explicit non-private constructor. 455 */ 456 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) { 457 // check if subclassing is prevented by having only private ctors 458 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK); 459 460 boolean hasDefaultConstructor = true; 461 boolean hasExplicitNonPrivateCtor = false; 462 463 DetailAST candidate = objBlock.getFirstChild(); 464 465 while (candidate != null) { 466 if (candidate.getType() == TokenTypes.CTOR_DEF) { 467 hasDefaultConstructor = false; 468 469 final DetailAST ctorMods = 470 candidate.findFirstToken(TokenTypes.MODIFIERS); 471 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 472 hasExplicitNonPrivateCtor = true; 473 break; 474 } 475 } 476 candidate = candidate.getNextSibling(); 477 } 478 479 return hasDefaultConstructor || hasExplicitNonPrivateCtor; 480 } 481 482 }