001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.Scope;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
034
035/**
036 * The check finds classes that are designed for extension (subclass creation).
037 *
038 * <p>
039 * Nothing wrong could be with founded classes.
040 * This check makes sense only for library projects (not an application projects)
041 * which care of ideal OOP-design to make sure that class works in all cases even misusage.
042 * Even in library projects this check most likely will find classes that are designed for extension
043 * by somebody. User needs to use suppressions extensively to got a benefit from this check,
044 * and keep in suppressions all confirmed/known classes that are deigned for inheritance
045 * intentionally to let the check catch only new classes, and bring this to team/user attention.
046 * </p>
047 *
048 * <p>
049 * ATTENTION: Only user can decide whether a class is designed for extension or not.
050 * The check just shows all classes which are possibly designed for extension.
051 * If smth inappropriate is found please use suppression.
052 * </p>
053 *
054 * <p>
055 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
056 * (a good practise is to explain its self-use of overridable methods) the check will not
057 * rise a violation. The violation can also be skipped if the method which can be overridden
058 * in a subclass has one or more annotations that are specified in ignoredAnnotations
059 * option. Note, that by default @Override annotation is not included in the
060 * ignoredAnnotations set as in a subclass the method which has the annotation can also be
061 * overridden in its subclass.
062 * </p>
063 *
064 * <p>
065 * More specifically, the check enforces a programming style where superclasses provide empty
066 * "hooks" that can be implemented by subclasses.
067 * </p>
068 *
069 * <p>
070 * The check finds classes that have overridable methods (public or protected methods
071 * that are non-static, not-final, non-abstract) and have non-empty implementation.
072 * </p>
073 *
074 * <p>
075 * This protects superclasses against being broken by subclasses. The downside is that subclasses
076 * are limited in their flexibility, in particular, they cannot prevent execution of code in the
077 * superclass, but that also means that subclasses cannot forget to call their super method.
078 * </p>
079 *
080 * <p>
081 * The check has the following options:
082 * </p>
083 * <ul>
084 * <li>
085 * ignoredAnnotations - annotations which allow the check to skip the method from validation.
086 * Default value is <b>Test, Before, After, BeforeClass, AfterClass</b>.
087 * </li>
088 * </ul>
089 *
090 * @author lkuehne
091 * @author Andrei Selkin
092 */
093public class DesignForExtensionCheck extends AbstractCheck {
094
095    /**
096     * A key is pointing to the warning message text in "messages.properties"
097     * file.
098     */
099    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 getAcceptableTokens();
118    }
119
120    @Override
121    public int[] getAcceptableTokens() {
122        // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
123        // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
124        // stack to hold CLASS_DEF tokens.
125        return new int[] {TokenTypes.METHOD_DEF};
126    }
127
128    @Override
129    public int[] getRequiredTokens() {
130        return getAcceptableTokens();
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
146            final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
147            if (canBeSubclassed(classDef)) {
148                final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
149                final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
150                log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY, className, methodName);
151            }
152        }
153    }
154
155    /**
156     * Checks whether a method has a javadoc comment.
157     * @param methodDef method definition token.
158     * @return true if a method has a javadoc comment.
159     */
160    private boolean hasJavadocComment(DetailAST methodDef) {
161        final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
162        return modifiers.branchContains(TokenTypes.BLOCK_COMMENT_BEGIN);
163    }
164
165    /**
166     * Checks whether a methods is native.
167     * @param ast method definition token.
168     * @return true if a methods is native.
169     */
170    private boolean isNativeMethod(DetailAST ast) {
171        final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
172        return mods.branchContains(TokenTypes.LITERAL_NATIVE);
173    }
174
175    /**
176     * Checks whether a method has only comments in the body (has an empty implementation).
177     * Method is OK if its implementation is empty.
178     * @param ast method definition token.
179     * @return true if a method has only comments in the body.
180     */
181    private static boolean hasEmptyImplementation(DetailAST ast) {
182        boolean hasEmptyBody = true;
183        final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
184        final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
185        final Predicate<DetailAST> predicate = currentNode -> {
186            return currentNode != methodImplCloseBrace
187                && !TokenUtils.isCommentType(currentNode.getType());
188        };
189        final Optional<DetailAST> methodBody =
190            TokenUtils.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
191        if (methodBody.isPresent()) {
192            hasEmptyBody = false;
193        }
194        return hasEmptyBody;
195    }
196
197    /**
198     * Checks whether a method can be overridden.
199     * Method can be overridden if it is not private, abstract, final or static.
200     * Note that the check has nothing to do for interfaces.
201     * @param methodDef method definition token.
202     * @return true if a method can be overridden in a subclass.
203     */
204    private boolean canBeOverridden(DetailAST methodDef) {
205        final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
206        return ScopeUtils.getSurroundingScope(methodDef).isIn(Scope.PROTECTED)
207            && !ScopeUtils.isInInterfaceOrAnnotationBlock(methodDef)
208            && !modifiers.branchContains(TokenTypes.LITERAL_PRIVATE)
209            && !modifiers.branchContains(TokenTypes.ABSTRACT)
210            && !modifiers.branchContains(TokenTypes.FINAL)
211            && !modifiers.branchContains(TokenTypes.LITERAL_STATIC);
212    }
213
214    /**
215     * Checks whether a method has any of ignored annotations.
216     * @param methodDef method definition token.
217     * @param annotations a set of ignored annotations.
218     * @return true if a method has any of ignored annotations.
219     */
220    private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
221        final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
222        boolean hasIgnoredAnnotation = false;
223        if (modifiers.branchContains(TokenTypes.ANNOTATION)) {
224            final Optional<DetailAST> annotation = TokenUtils.findFirstTokenByPredicate(modifiers,
225                currentToken -> {
226                    return currentToken.getType() == TokenTypes.ANNOTATION
227                        && annotations.contains(getAnnotationName(currentToken));
228                });
229            if (annotation.isPresent()) {
230                hasIgnoredAnnotation = true;
231            }
232        }
233        return hasIgnoredAnnotation;
234    }
235
236    /**
237     * Gets the name of the annotation.
238     * @param annotation to get name of.
239     * @return the name of the annotation.
240     */
241    private static String getAnnotationName(DetailAST annotation) {
242        final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
243        final String name;
244        if (dotAst == null) {
245            name = annotation.findFirstToken(TokenTypes.IDENT).getText();
246        }
247        else {
248            name = dotAst.findFirstToken(TokenTypes.IDENT).getText();
249        }
250        return name;
251    }
252
253    /**
254     * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
255     * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
256     * @param ast the start node for searching.
257     * @return the CLASS_DEF or ENUM_DEF token.
258     */
259    private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
260        DetailAST searchAST = ast;
261        while (searchAST.getType() != TokenTypes.CLASS_DEF
262               && searchAST.getType() != TokenTypes.ENUM_DEF) {
263            searchAST = searchAST.getParent();
264        }
265        return searchAST;
266    }
267
268    /**
269     * Checks if the given class (given CLASS_DEF node) can be subclassed.
270     * @param classDef class definition token.
271     * @return true if the containing class can be subclassed.
272     */
273    private static boolean canBeSubclassed(DetailAST classDef) {
274        final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
275        return classDef.getType() != TokenTypes.ENUM_DEF
276            && !modifiers.branchContains(TokenTypes.FINAL)
277            && hasDefaultOrExplicitNonPrivateCtor(classDef);
278    }
279
280    /**
281     * Checks whether a class has default or explicit non-private constructor.
282     * @param classDef class ast token.
283     * @return true if a class has default or explicit non-private constructor.
284     */
285    private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
286        // check if subclassing is prevented by having only private ctors
287        final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
288
289        boolean hasDefaultConstructor = true;
290        boolean hasExplicitNonPrivateCtor = false;
291
292        DetailAST candidate = objBlock.getFirstChild();
293
294        while (candidate != null) {
295            if (candidate.getType() == TokenTypes.CTOR_DEF) {
296                hasDefaultConstructor = false;
297
298                final DetailAST ctorMods =
299                        candidate.findFirstToken(TokenTypes.MODIFIERS);
300                if (!ctorMods.branchContains(TokenTypes.LITERAL_PRIVATE)) {
301                    hasExplicitNonPrivateCtor = true;
302                    break;
303                }
304            }
305            candidate = candidate.getNextSibling();
306        }
307
308        return hasDefaultConstructor || hasExplicitNonPrivateCtor;
309    }
310}