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