View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 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.Optional;
24  import java.util.Set;
25  import java.util.function.Predicate;
26  import java.util.stream.Collectors;
27  
28  import com.puppycrawl.tools.checkstyle.StatelessCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.Scope;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
34  import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
35  
36  /**
37   * The check finds classes that are designed for extension (subclass creation).
38   *
39   * <p>
40   * Nothing wrong could be with founded classes.
41   * This check makes sense only for library projects (not application projects)
42   * which care of ideal OOP-design to make sure that class works in all cases even misusage.
43   * Even in library projects this check most likely will find classes that are designed for extension
44   * by somebody. User needs to use suppressions extensively to got a benefit from this check,
45   * and keep in suppressions all confirmed/known classes that are deigned for inheritance
46   * intentionally to let the check catch only new classes, and bring this to team/user attention.
47   * </p>
48   *
49   * <p>
50   * ATTENTION: Only user can decide whether a class is designed for extension or not.
51   * The check just shows all classes which are possibly designed for extension.
52   * If smth inappropriate is found please use suppression.
53   * </p>
54   *
55   * <p>
56   * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
57   * (a good practice is to explain its self-use of overridable methods) the check will not
58   * rise a violation. The violation can also be skipped if the method which can be overridden
59   * in a subclass has one or more annotations that are specified in ignoredAnnotations
60   * option. Note, that by default @Override annotation is not included in the
61   * ignoredAnnotations set as in a subclass the method which has the annotation can also be
62   * overridden in its subclass.
63   * </p>
64   *
65   * <p>
66   * More specifically, the check enforces a programming style where superclasses provide empty
67   * "hooks" that can be implemented by subclasses.
68   * </p>
69   *
70   * <p>
71   * The check finds classes that have overridable methods (public or protected methods
72   * that are non-static, not-final, non-abstract) and have non-empty implementation.
73   * </p>
74   *
75   * <p>
76   * This protects superclasses against being broken by subclasses. The downside is that subclasses
77   * are limited in their flexibility, in particular, they cannot prevent execution of code in the
78   * superclass, but that also means that subclasses cannot forget to call their super method.
79   * </p>
80   *
81   * <p>
82   * The check has the following options:
83   * </p>
84   * <ul>
85   * <li>
86   * ignoredAnnotations - annotations which allow the check to skip the method from validation.
87   * Default value is <b>Test, Before, After, BeforeClass, AfterClass</b>.
88   * </li>
89   * </ul>
90   *
91   * @author lkuehne
92   * @author Andrei Selkin
93   */
94  @StatelessCheck
95  public class DesignForExtensionCheck extends AbstractCheck {
96  
97      /**
98       * A key is pointing to the warning message text in "messages.properties"
99       * file.
100      */
101     public static final String MSG_KEY = "design.forExtension";
102 
103     /**
104      * A set of annotations which allow the check to skip the method from validation.
105      */
106     private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
107         "BeforeClass", "AfterClass", }).collect(Collectors.toSet());
108 
109     /**
110      * Sets annotations which allow the check to skip the method from validation.
111      * @param ignoredAnnotations method annotations.
112      */
113     public void setIgnoredAnnotations(String... ignoredAnnotations) {
114         this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet());
115     }
116 
117     @Override
118     public int[] getDefaultTokens() {
119         return getRequiredTokens();
120     }
121 
122     @Override
123     public int[] getAcceptableTokens() {
124         return getRequiredTokens();
125     }
126 
127     @Override
128     public int[] getRequiredTokens() {
129         // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
130         // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
131         // stack to hold CLASS_DEF tokens.
132         return new int[] {TokenTypes.METHOD_DEF};
133     }
134 
135     @Override
136     public boolean isCommentNodesRequired() {
137         return true;
138     }
139 
140     @Override
141     public void visitToken(DetailAST ast) {
142         if (!hasJavadocComment(ast)
143                 && canBeOverridden(ast)
144                 && (isNativeMethod(ast)
145                     || !hasEmptyImplementation(ast))
146                 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) {
147             final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
148             if (canBeSubclassed(classDef)) {
149                 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
150                 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
151                 log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY, className, methodName);
152             }
153         }
154     }
155 
156     /**
157      * Checks whether a method has a javadoc comment.
158      * @param methodDef method definition token.
159      * @return true if a method has a javadoc comment.
160      */
161     private static boolean hasJavadocComment(DetailAST methodDef) {
162         return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
163                 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
164     }
165 
166     /**
167      * Checks whether a token has a javadoc comment.
168      *
169      * @param methodDef method definition token.
170      * @param tokenType token type.
171      * @return true if a token has a javadoc comment.
172      */
173     private static boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) {
174         final DetailAST token = methodDef.findFirstToken(tokenType);
175         return token.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN);
176     }
177 
178     /**
179      * Checks whether a methods is native.
180      * @param ast method definition token.
181      * @return true if a methods is native.
182      */
183     private static boolean isNativeMethod(DetailAST ast) {
184         final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
185         return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
186     }
187 
188     /**
189      * Checks whether a method has only comments in the body (has an empty implementation).
190      * Method is OK if its implementation is empty.
191      * @param ast method definition token.
192      * @return true if a method has only comments in the body.
193      */
194     private static boolean hasEmptyImplementation(DetailAST ast) {
195         boolean hasEmptyBody = true;
196         final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
197         final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
198         final Predicate<DetailAST> predicate = currentNode -> {
199             return currentNode != methodImplCloseBrace
200                 && !TokenUtils.isCommentType(currentNode.getType());
201         };
202         final Optional<DetailAST> methodBody =
203             TokenUtils.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
204         if (methodBody.isPresent()) {
205             hasEmptyBody = false;
206         }
207         return hasEmptyBody;
208     }
209 
210     /**
211      * Checks whether a method can be overridden.
212      * Method can be overridden if it is not private, abstract, final or static.
213      * Note that the check has nothing to do for interfaces.
214      * @param methodDef method definition token.
215      * @return true if a method can be overridden in a subclass.
216      */
217     private static boolean canBeOverridden(DetailAST methodDef) {
218         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
219         return ScopeUtils.getSurroundingScope(methodDef).isIn(Scope.PROTECTED)
220             && !ScopeUtils.isInInterfaceOrAnnotationBlock(methodDef)
221             && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
222             && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null
223             && modifiers.findFirstToken(TokenTypes.FINAL) == null
224             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null;
225     }
226 
227     /**
228      * Checks whether a method has any of ignored annotations.
229      * @param methodDef method definition token.
230      * @param annotations a set of ignored annotations.
231      * @return true if a method has any of ignored annotations.
232      */
233     private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
234         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
235         boolean hasIgnoredAnnotation = false;
236         if (modifiers.findFirstToken(TokenTypes.ANNOTATION) != null) {
237             final Optional<DetailAST> annotation = TokenUtils.findFirstTokenByPredicate(modifiers,
238                 currentToken -> {
239                     return currentToken.getType() == TokenTypes.ANNOTATION
240                         && annotations.contains(getAnnotationName(currentToken));
241                 });
242             if (annotation.isPresent()) {
243                 hasIgnoredAnnotation = true;
244             }
245         }
246         return hasIgnoredAnnotation;
247     }
248 
249     /**
250      * Gets the name of the annotation.
251      * @param annotation to get name of.
252      * @return the name of the annotation.
253      */
254     private static String getAnnotationName(DetailAST annotation) {
255         final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
256         final String name;
257         if (dotAst == null) {
258             name = annotation.findFirstToken(TokenTypes.IDENT).getText();
259         }
260         else {
261             name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
262         }
263         return name;
264     }
265 
266     /**
267      * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
268      * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
269      * @param ast the start node for searching.
270      * @return the CLASS_DEF or ENUM_DEF token.
271      */
272     private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
273         DetailAST searchAST = ast;
274         while (searchAST.getType() != TokenTypes.CLASS_DEF
275                && searchAST.getType() != TokenTypes.ENUM_DEF) {
276             searchAST = searchAST.getParent();
277         }
278         return searchAST;
279     }
280 
281     /**
282      * Checks if the given class (given CLASS_DEF node) can be subclassed.
283      * @param classDef class definition token.
284      * @return true if the containing class can be subclassed.
285      */
286     private static boolean canBeSubclassed(DetailAST classDef) {
287         final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
288         return classDef.getType() != TokenTypes.ENUM_DEF
289             && modifiers.findFirstToken(TokenTypes.FINAL) == null
290             && hasDefaultOrExplicitNonPrivateCtor(classDef);
291     }
292 
293     /**
294      * Checks whether a class has default or explicit non-private constructor.
295      * @param classDef class ast token.
296      * @return true if a class has default or explicit non-private constructor.
297      */
298     private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
299         // check if subclassing is prevented by having only private ctors
300         final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
301 
302         boolean hasDefaultConstructor = true;
303         boolean hasExplicitNonPrivateCtor = false;
304 
305         DetailAST candidate = objBlock.getFirstChild();
306 
307         while (candidate != null) {
308             if (candidate.getType() == TokenTypes.CTOR_DEF) {
309                 hasDefaultConstructor = false;
310 
311                 final DetailAST ctorMods =
312                         candidate.findFirstToken(TokenTypes.MODIFIERS);
313                 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
314                     hasExplicitNonPrivateCtor = true;
315                     break;
316                 }
317             }
318             candidate = candidate.getNextSibling();
319         }
320 
321         return hasDefaultConstructor || hasExplicitNonPrivateCtor;
322     }
323 
324 }