001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.Objects; 024import java.util.Optional; 025import java.util.Set; 026import java.util.function.Predicate; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029import java.util.stream.Collectors; 030 031import com.puppycrawl.tools.checkstyle.StatelessCheck; 032import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 033import com.puppycrawl.tools.checkstyle.api.DetailAST; 034import com.puppycrawl.tools.checkstyle.api.Scope; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 037import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 038import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 039 040/** 041 * <div> 042 * Checks that classes are designed for extension (subclass creation). 043 * </div> 044 * 045 * <p> 046 * Nothing wrong could be with founded classes. 047 * This check makes sense only for library projects (not application projects) 048 * which care of ideal OOP-design to make sure that class works in all cases even misusage. 049 * Even in library projects this check most likely will find classes that are designed for extension 050 * by somebody. User needs to use suppressions extensively to got a benefit from this check, 051 * and keep in suppressions all confirmed/known classes that are deigned for inheritance 052 * intentionally to let the check catch only new classes, and bring this to team/user attention. 053 * </p> 054 * 055 * <p> 056 * ATTENTION: Only user can decide whether a class is designed for extension or not. 057 * The check just shows all classes which are possibly designed for extension. 058 * If something inappropriate is found please use suppression. 059 * </p> 060 * 061 * <p> 062 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment 063 * (a good practice is to explain its self-use of overridable methods) the check will not 064 * rise a violation. The violation can also be skipped if the method which can be overridden 065 * in a subclass has one or more annotations that are specified in ignoredAnnotations 066 * option. Note, that by default @Override annotation is not included in the 067 * ignoredAnnotations set as in a subclass the method which has the annotation can also be 068 * overridden in its subclass. 069 * </p> 070 * 071 * <p> 072 * Problem is described at "Effective Java, 2nd Edition by Joshua Bloch" book, chapter 073 * "Item 17: Design and document for inheritance or else prohibit it". 074 * </p> 075 * 076 * <p> 077 * Some quotes from book: 078 * </p> 079 * <blockquote>The class must document its self-use of overridable methods. 080 * By convention, a method that invokes overridable methods contains a description 081 * of these invocations at the end of its documentation comment. The description 082 * begins with the phrase “This implementation.” 083 * </blockquote> 084 * <blockquote> 085 * The best solution to this problem is to prohibit subclassing in classes that 086 * are not designed and documented to be safely subclassed. 087 * </blockquote> 088 * <blockquote> 089 * If a concrete class does not implement a standard interface, then you may 090 * inconvenience some programmers by prohibiting inheritance. If you feel that you 091 * must allow inheritance from such a class, one reasonable approach is to ensure 092 * that the class never invokes any of its overridable methods and to document this 093 * fact. In other words, eliminate the class’s self-use of overridable methods entirely. 094 * In doing so, you’ll create a class that is reasonably safe to subclass. Overriding a 095 * method will never affect the behavior of any other method. 096 * </blockquote> 097 * 098 * <p> 099 * 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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 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 * </code></pre></div> 146 * 147 * <p> 148 * Example of code without violation: 149 * </p> 150 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 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 * </code></pre></div> 166 * 167 * @since 3.1 168 */ 169@StatelessCheck 170public class DesignForExtensionCheck extends AbstractCheck { 171 172 /** 173 * A key is pointing to the warning message text in "messages.properties" 174 * file. 175 */ 176 public static final String MSG_KEY = "design.forExtension"; 177 178 /** 179 * Specify annotations which allow the check to skip the method from validation. 180 */ 181 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After", 182 "BeforeClass", "AfterClass", }).collect(Collectors.toUnmodifiableSet()); 183 184 /** 185 * Specify the comment text pattern which qualifies a method as designed for extension. 186 * Supports multi-line regex. 187 */ 188 private Pattern requiredJavadocPhrase = Pattern.compile(".*"); 189 190 /** 191 * Creates a new {@code DesignForExtensionCheck} instance. 192 */ 193 public DesignForExtensionCheck() { 194 // no code by default 195 } 196 197 /** 198 * Setter to specify annotations which allow the check to skip the method from validation. 199 * 200 * @param ignoredAnnotations method annotations. 201 * @since 7.2 202 */ 203 public void setIgnoredAnnotations(String... ignoredAnnotations) { 204 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations) 205 .collect(Collectors.toUnmodifiableSet()); 206 } 207 208 /** 209 * Setter to specify the comment text pattern which qualifies a 210 * method as designed for extension. Supports multi-line regex. 211 * 212 * @param requiredJavadocPhrase method annotations. 213 * @since 8.40 214 */ 215 public void setRequiredJavadocPhrase(Pattern requiredJavadocPhrase) { 216 this.requiredJavadocPhrase = requiredJavadocPhrase; 217 } 218 219 @Override 220 public int[] getDefaultTokens() { 221 return getRequiredTokens(); 222 } 223 224 @Override 225 public int[] getAcceptableTokens() { 226 return getRequiredTokens(); 227 } 228 229 @Override 230 public int[] getRequiredTokens() { 231 // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check 232 // subscribes to CLASS_DEF token it will become stateful, since we need to have additional 233 // stack to hold CLASS_DEF tokens. 234 return new int[] {TokenTypes.METHOD_DEF}; 235 } 236 237 @Override 238 public boolean isCommentNodesRequired() { 239 return true; 240 } 241 242 @Override 243 public void visitToken(DetailAST ast) { 244 if (!hasJavadocComment(ast) 245 && canBeOverridden(ast) 246 && (isNativeMethod(ast) 247 || !hasEmptyImplementation(ast)) 248 && !hasIgnoredAnnotation(ast, ignoredAnnotations) 249 && !ScopeUtil.isInRecordBlock(ast)) { 250 final DetailAST classDef = getNearestClassOrEnumDefinition(ast); 251 if (canBeSubclassed(classDef)) { 252 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText(); 253 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText(); 254 log(ast, MSG_KEY, className, methodName); 255 } 256 } 257 } 258 259 /** 260 * Checks whether a method has a javadoc comment. 261 * 262 * @param methodDef method definition token. 263 * @return true if a method has a javadoc comment. 264 */ 265 private boolean hasJavadocComment(DetailAST methodDef) { 266 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS) 267 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE); 268 } 269 270 /** 271 * Checks whether a token has a javadoc comment. 272 * 273 * @param methodDef method definition token. 274 * @param tokenType token type. 275 * @return true if a token has a javadoc comment. 276 */ 277 private boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) { 278 final DetailAST token = methodDef.findFirstToken(tokenType); 279 return branchContainsJavadocComment(token); 280 } 281 282 /** 283 * Checks whether a javadoc comment exists under the token. 284 * 285 * @param token tree token. 286 * @return true if a javadoc comment exists under the token. 287 */ 288 private boolean branchContainsJavadocComment(DetailAST token) { 289 boolean result = false; 290 DetailAST curNode = token; 291 while (curNode != null) { 292 if (curNode.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 293 && JavadocUtil.isJavadocComment(curNode)) { 294 result = hasValidJavadocComment(curNode); 295 break; 296 } 297 298 DetailAST toVisit = curNode.getFirstChild(); 299 while (toVisit == null) { 300 if (curNode == token) { 301 break; 302 } 303 304 toVisit = curNode.getNextSibling(); 305 curNode = curNode.getParent(); 306 } 307 curNode = toVisit; 308 } 309 310 return result; 311 } 312 313 /** 314 * Checks whether a javadoc contains the specified comment pattern that denotes 315 * a method as designed for extension. 316 * 317 * @param detailAST the ast we are checking for possible extension 318 * @return true if the javadoc of this ast contains the required comment pattern 319 */ 320 private boolean hasValidJavadocComment(DetailAST detailAST) { 321 final String javadocString = 322 JavadocUtil.getBlockCommentContent(detailAST); 323 324 final Matcher requiredJavadocPhraseMatcher = 325 requiredJavadocPhrase.matcher(javadocString); 326 327 return requiredJavadocPhraseMatcher.find(); 328 } 329 330 /** 331 * Checks whether a method is native. 332 * 333 * @param ast method definition token. 334 * @return true if a methods is native. 335 */ 336 private static boolean isNativeMethod(DetailAST ast) { 337 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS); 338 return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null; 339 } 340 341 /** 342 * Checks whether a method has only comments in the body (has an empty implementation). 343 * Method is OK if its implementation is empty. 344 * 345 * @param ast method definition token. 346 * @return true if a method has only comments in the body. 347 */ 348 private static boolean hasEmptyImplementation(DetailAST ast) { 349 boolean hasEmptyBody = true; 350 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST); 351 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild(); 352 final Predicate<DetailAST> predicate = currentNode -> { 353 return currentNode != methodImplCloseBrace 354 && !TokenUtil.isCommentType(currentNode.getType()); 355 }; 356 final Optional<DetailAST> methodBody = 357 TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate); 358 if (methodBody.isPresent()) { 359 hasEmptyBody = false; 360 } 361 return hasEmptyBody; 362 } 363 364 /** 365 * Checks whether a method can be overridden. 366 * Method can be overridden if it is not private, abstract, final or static. 367 * Note that the check has nothing to do for interfaces. 368 * 369 * @param methodDef method definition token. 370 * @return true if a method can be overridden in a subclass. 371 */ 372 private static boolean canBeOverridden(DetailAST methodDef) { 373 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 374 return ScopeUtil.getSurroundingScope(methodDef) 375 .map(surroundingScope -> { 376 return surroundingScope.isIn(Scope.PROTECTED) 377 && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef) 378 && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null 379 && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null 380 && modifiers.findFirstToken(TokenTypes.FINAL) == null 381 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null; 382 }) 383 .orElse(Boolean.FALSE); 384 } 385 386 /** 387 * Checks whether a method has any of ignored annotations. 388 * 389 * @param methodDef method definition token. 390 * @param annotations a set of ignored annotations. 391 * @return true if a method has any of ignored annotations. 392 */ 393 private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) { 394 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS); 395 final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers, 396 currentToken -> { 397 return currentToken.getType() == TokenTypes.ANNOTATION 398 && annotations.contains(getAnnotationName(currentToken)); 399 }); 400 return annotation.isPresent(); 401 } 402 403 /** 404 * Gets the name of the annotation. 405 * 406 * @param annotation to get name of. 407 * @return the name of the annotation. 408 */ 409 private static String getAnnotationName(DetailAST annotation) { 410 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT); 411 final DetailAST parent = Objects.requireNonNullElse(dotAst, annotation); 412 return parent.findFirstToken(TokenTypes.IDENT).getText(); 413 } 414 415 /** 416 * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node. 417 * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node. 418 * 419 * @param ast the start node for searching. 420 * @return the CLASS_DEF or ENUM_DEF token. 421 */ 422 private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) { 423 DetailAST searchAST = ast; 424 while (searchAST.getType() != TokenTypes.CLASS_DEF 425 && searchAST.getType() != TokenTypes.ENUM_DEF) { 426 searchAST = searchAST.getParent(); 427 } 428 return searchAST; 429 } 430 431 /** 432 * Checks if the given class (given CLASS_DEF node) can be subclassed. 433 * 434 * @param classDef class definition token. 435 * @return true if the containing class can be subclassed. 436 */ 437 private static boolean canBeSubclassed(DetailAST classDef) { 438 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS); 439 return classDef.getType() != TokenTypes.ENUM_DEF 440 && modifiers.findFirstToken(TokenTypes.FINAL) == null 441 && hasDefaultOrExplicitNonPrivateCtor(classDef); 442 } 443 444 /** 445 * Checks whether a class has default or explicit non-private constructor. 446 * 447 * @param classDef class ast token. 448 * @return true if a class has default or explicit non-private constructor. 449 */ 450 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) { 451 // check if subclassing is prevented by having only private ctors 452 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK); 453 454 boolean hasDefaultConstructor = true; 455 boolean hasExplicitNonPrivateCtor = false; 456 457 DetailAST candidate = objBlock.getFirstChild(); 458 459 while (candidate != null) { 460 if (candidate.getType() == TokenTypes.CTOR_DEF) { 461 hasDefaultConstructor = false; 462 463 final DetailAST ctorMods = 464 candidate.findFirstToken(TokenTypes.MODIFIERS); 465 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 466 hasExplicitNonPrivateCtor = true; 467 break; 468 } 469 } 470 candidate = candidate.getNextSibling(); 471 } 472 473 return hasDefaultConstructor || hasExplicitNonPrivateCtor; 474 } 475 476}