1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.ScopeUtil;
34  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
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   */
92  @StatelessCheck
93  public class DesignForExtensionCheck extends AbstractCheck {
94  
95      /**
96       * A key is pointing to the warning message text in "messages.properties"
97       * file.
98       */
99      public static final String MSG_KEY = "design.forExtension";
100 
101     /**
102      * A set of annotations which allow the check to skip the method from validation.
103      */
104     private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
105         "BeforeClass", "AfterClass", }).collect(Collectors.toSet());
106 
107     /**
108      * Sets annotations which allow the check to skip the method from validation.
109      * @param ignoredAnnotations method annotations.
110      */
111     public void setIgnoredAnnotations(String... ignoredAnnotations) {
112         this.ignoredAnnotations = Arrays.stream(ignoredAnnotations).collect(Collectors.toSet());
113     }
114 
115     @Override
116     public int[] getDefaultTokens() {
117         return getRequiredTokens();
118     }
119 
120     @Override
121     public int[] getAcceptableTokens() {
122         return getRequiredTokens();
123     }
124 
125     @Override
126     public int[] getRequiredTokens() {
127         // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
128         // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
129         // stack to hold CLASS_DEF tokens.
130         return new int[] {TokenTypes.METHOD_DEF};
131     }
132 
133     @Override
134     public boolean isCommentNodesRequired() {
135         return true;
136     }
137 
138     @Override
139     public void visitToken(DetailAST ast) {
140         if (!hasJavadocComment(ast)
141                 && canBeOverridden(ast)
142                 && (isNativeMethod(ast)
143                     || !hasEmptyImplementation(ast))
144                 && !hasIgnoredAnnotation(ast, ignoredAnnotations)) {
145             final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
146             if (canBeSubclassed(classDef)) {
147                 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
148                 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
149                 log(ast, MSG_KEY, className, methodName);
150             }
151         }
152     }
153 
154     /**
155      * Checks whether a method has a javadoc comment.
156      * @param methodDef method definition token.
157      * @return true if a method has a javadoc comment.
158      */
159     private static boolean hasJavadocComment(DetailAST methodDef) {
160         return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
161                 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
162     }
163 
164     /**
165      * Checks whether a token has a javadoc comment.
166      *
167      * @param methodDef method definition token.
168      * @param tokenType token type.
169      * @return true if a token has a javadoc comment.
170      */
171     private static boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) {
172         final DetailAST token = methodDef.findFirstToken(tokenType);
173         return token.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN);
174     }
175 
176     /**
177      * Checks whether a methods is native.
178      * @param ast method definition token.
179      * @return true if a methods is native.
180      */
181     private static boolean isNativeMethod(DetailAST ast) {
182         final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
183         return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
184     }
185 
186     /**
187      * Checks whether a method has only comments in the body (has an empty implementation).
188      * Method is OK if its implementation is empty.
189      * @param ast method definition token.
190      * @return true if a method has only comments in the body.
191      */
192     private static boolean hasEmptyImplementation(DetailAST ast) {
193         boolean hasEmptyBody = true;
194         final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
195         final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
196         final Predicate<DetailAST> predicate = currentNode -> {
197             return currentNode != methodImplCloseBrace
198                 && !TokenUtil.isCommentType(currentNode.getType());
199         };
200         final Optional<DetailAST> methodBody =
201             TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
202         if (methodBody.isPresent()) {
203             hasEmptyBody = false;
204         }
205         return hasEmptyBody;
206     }
207 
208     /**
209      * Checks whether a method can be overridden.
210      * Method can be overridden if it is not private, abstract, final or static.
211      * Note that the check has nothing to do for interfaces.
212      * @param methodDef method definition token.
213      * @return true if a method can be overridden in a subclass.
214      */
215     private static boolean canBeOverridden(DetailAST methodDef) {
216         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
217         return ScopeUtil.getSurroundingScope(methodDef).isIn(Scope.PROTECTED)
218             && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef)
219             && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
220             && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null
221             && modifiers.findFirstToken(TokenTypes.FINAL) == null
222             && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null;
223     }
224 
225     /**
226      * Checks whether a method has any of ignored annotations.
227      * @param methodDef method definition token.
228      * @param annotations a set of ignored annotations.
229      * @return true if a method has any of ignored annotations.
230      */
231     private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
232         final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
233         final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers,
234             currentToken -> {
235                 return currentToken.getType() == TokenTypes.ANNOTATION
236                     && annotations.contains(getAnnotationName(currentToken));
237             });
238         return annotation.isPresent();
239     }
240 
241     /**
242      * Gets the name of the annotation.
243      * @param annotation to get name of.
244      * @return the name of the annotation.
245      */
246     private static String getAnnotationName(DetailAST annotation) {
247         final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
248         final String name;
249         if (dotAst == null) {
250             name = annotation.findFirstToken(TokenTypes.IDENT).getText();
251         }
252         else {
253             name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
254         }
255         return name;
256     }
257 
258     /**
259      * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
260      * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
261      * @param ast the start node for searching.
262      * @return the CLASS_DEF or ENUM_DEF token.
263      */
264     private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
265         DetailAST searchAST = ast;
266         while (searchAST.getType() != TokenTypes.CLASS_DEF
267                && searchAST.getType() != TokenTypes.ENUM_DEF) {
268             searchAST = searchAST.getParent();
269         }
270         return searchAST;
271     }
272 
273     /**
274      * Checks if the given class (given CLASS_DEF node) can be subclassed.
275      * @param classDef class definition token.
276      * @return true if the containing class can be subclassed.
277      */
278     private static boolean canBeSubclassed(DetailAST classDef) {
279         final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
280         return classDef.getType() != TokenTypes.ENUM_DEF
281             && modifiers.findFirstToken(TokenTypes.FINAL) == null
282             && hasDefaultOrExplicitNonPrivateCtor(classDef);
283     }
284 
285     /**
286      * Checks whether a class has default or explicit non-private constructor.
287      * @param classDef class ast token.
288      * @return true if a class has default or explicit non-private constructor.
289      */
290     private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
291         // check if subclassing is prevented by having only private ctors
292         final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
293 
294         boolean hasDefaultConstructor = true;
295         boolean hasExplicitNonPrivateCtor = false;
296 
297         DetailAST candidate = objBlock.getFirstChild();
298 
299         while (candidate != null) {
300             if (candidate.getType() == TokenTypes.CTOR_DEF) {
301                 hasDefaultConstructor = false;
302 
303                 final DetailAST ctorMods =
304                         candidate.findFirstToken(TokenTypes.MODIFIERS);
305                 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
306                     hasExplicitNonPrivateCtor = true;
307                     break;
308                 }
309             }
310             candidate = candidate.getNextSibling();
311         }
312 
313         return hasDefaultConstructor || hasExplicitNonPrivateCtor;
314     }
315 
316 }