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