View Javadoc
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  *   &#64;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 }