001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.Optional;
024import java.util.Set;
025import java.util.function.Predicate;
026import java.util.stream.Collectors;
027
028import com.puppycrawl.tools.checkstyle.StatelessCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.Scope;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
034import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
035
036/**
037 * The check finds classes that are designed for extension (subclass creation).
038 *
039 * <p>
040 * Nothing wrong could be with founded classes.
041 * This check makes sense only for library projects (not application projects)
042 * which care of ideal OOP-design to make sure that class works in all cases even misusage.
043 * Even in library projects this check most likely will find classes that are designed for extension
044 * by somebody. User needs to use suppressions extensively to got a benefit from this check,
045 * and keep in suppressions all confirmed/known classes that are deigned for inheritance
046 * intentionally to let the check catch only new classes, and bring this to team/user attention.
047 * </p>
048 *
049 * <p>
050 * ATTENTION: Only user can decide whether a class is designed for extension or not.
051 * The check just shows all classes which are possibly designed for extension.
052 * If smth inappropriate is found please use suppression.
053 * </p>
054 *
055 * <p>
056 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
057 * (a good practice is to explain its self-use of overridable methods) the check will not
058 * rise a violation. The violation can also be skipped if the method which can be overridden
059 * in a subclass has one or more annotations that are specified in ignoredAnnotations
060 * option. Note, that by default @Override annotation is not included in the
061 * ignoredAnnotations set as in a subclass the method which has the annotation can also be
062 * overridden in its subclass.
063 * </p>
064 *
065 * <p>
066 * More specifically, the check enforces a programming style where superclasses provide empty
067 * "hooks" that can be implemented by subclasses.
068 * </p>
069 *
070 * <p>
071 * The check finds classes that have overridable methods (public or protected methods
072 * that are non-static, not-final, non-abstract) and have non-empty implementation.
073 * </p>
074 *
075 * <p>
076 * This protects superclasses against being broken by subclasses. The downside is that subclasses
077 * are limited in their flexibility, in particular, they cannot prevent execution of code in the
078 * superclass, but that also means that subclasses cannot forget to call their super method.
079 * </p>
080 *
081 * <p>
082 * The check has the following options:
083 * </p>
084 * <ul>
085 * <li>
086 * ignoredAnnotations - annotations which allow the check to skip the method from validation.
087 * Default value is <b>Test, Before, After, BeforeClass, AfterClass</b>.
088 * </li>
089 * </ul>
090 *
091 * @author lkuehne
092 * @author Andrei Selkin
093 */
094@StatelessCheck
095public class DesignForExtensionCheck extends AbstractCheck {
096
097    /**
098     * A key is pointing to the warning message text in "messages.properties"
099     * 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}