001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.Objects;
024import java.util.Optional;
025import java.util.Set;
026import java.util.function.Predicate;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import java.util.stream.Collectors;
030
031import com.puppycrawl.tools.checkstyle.StatelessCheck;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.DetailAST;
034import com.puppycrawl.tools.checkstyle.api.Scope;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
037import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
038import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
039
040/**
041 * <div>
042 * Checks that classes are designed for extension (subclass creation).
043 * </div>
044 *
045 * <p>
046 * Nothing wrong could be with founded classes.
047 * This check makes sense only for library projects (not application projects)
048 * which care of ideal OOP-design to make sure that class works in all cases even misusage.
049 * Even in library projects this check most likely will find classes that are designed for extension
050 * by somebody. User needs to use suppressions extensively to got a benefit from this check,
051 * and keep in suppressions all confirmed/known classes that are deigned for inheritance
052 * intentionally to let the check catch only new classes, and bring this to team/user attention.
053 * </p>
054 *
055 * <p>
056 * ATTENTION: Only user can decide whether a class is designed for extension or not.
057 * The check just shows all classes which are possibly designed for extension.
058 * If smth inappropriate is found please use suppression.
059 * </p>
060 *
061 * <p>
062 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
063 * (a good practice is to explain its self-use of overridable methods) the check will not
064 * rise a violation. The violation can also be skipped if the method which can be overridden
065 * in a subclass has one or more annotations that are specified in ignoredAnnotations
066 * option. Note, that by default @Override annotation is not included in the
067 * ignoredAnnotations set as in a subclass the method which has the annotation can also be
068 * overridden in its subclass.
069 * </p>
070 *
071 * <p>
072 * Problem is described at "Effective Java, 2nd Edition by Joshua Bloch" book, chapter
073 * "Item 17: Design and document for inheritance or else prohibit it".
074 * </p>
075 *
076 * <p>
077 * Some quotes from book:
078 * </p>
079 * <blockquote>The class must document its self-use of overridable methods.
080 * By convention, a method that invokes overridable methods contains a description
081 * of these invocations at the end of its documentation comment. The description
082 * begins with the phrase “This implementation.”
083 * </blockquote>
084 * <blockquote>
085 * The best solution to this problem is to prohibit subclassing in classes that
086 * are not designed and documented to be safely subclassed.
087 * </blockquote>
088 * <blockquote>
089 * If a concrete class does not implement a standard interface, then you may
090 * inconvenience some programmers by prohibiting inheritance. If you feel that you
091 * must allow inheritance from such a class, one reasonable approach is to ensure
092 * that the class never invokes any of its overridable methods and to document this
093 * fact. In other words, eliminate the class’s self-use of overridable methods entirely.
094 * In doing so, you’ll create a class that is reasonably safe to subclass. Overriding a
095 * method will never affect the behavior of any other method.
096 * </blockquote>
097 *
098 * <p>
099 * The check finds classes that have overridable methods (public or protected methods
100 * that are non-static, not-final, non-abstract) and have non-empty implementation.
101 * </p>
102 *
103 * <p>
104 * Rationale: This library design style protects superclasses against being broken
105 * by subclasses. The downside is that subclasses are limited in their flexibility,
106 * in particular they cannot prevent execution of code in the superclass, but that
107 * also means that subclasses cannot corrupt the state of the superclass by forgetting
108 * to call the superclass's method.
109 * </p>
110 *
111 * <p>
112 * More specifically, it enforces a programming style where superclasses provide
113 * empty "hooks" that can be implemented by subclasses.
114 * </p>
115 *
116 * <p>
117 * Example of code that cause violation as it is designed for extension:
118 * </p>
119 * <pre>
120 * public abstract class Plant {
121 *   private String roots;
122 *   private String trunk;
123 *
124 *   protected void validate() {
125 *     if (roots == null) throw new IllegalArgumentException("No roots!");
126 *     if (trunk == null) throw new IllegalArgumentException("No trunk!");
127 *   }
128 *
129 *   public abstract void grow();
130 * }
131 *
132 * public class Tree extends Plant {
133 *   private List leaves;
134 *
135 *   &#64;Overrides
136 *   protected void validate() {
137 *     super.validate();
138 *     if (leaves == null) throw new IllegalArgumentException("No leaves!");
139 *   }
140 *
141 *   public void grow() {
142 *     validate();
143 *   }
144 * }
145 * </pre>
146 *
147 * <p>
148 * Example of code without violation:
149 * </p>
150 * <pre>
151 * public abstract class Plant {
152 *   private String roots;
153 *   private String trunk;
154 *
155 *   private void validate() {
156 *     if (roots == null) throw new IllegalArgumentException("No roots!");
157 *     if (trunk == null) throw new IllegalArgumentException("No trunk!");
158 *     validateEx();
159 *   }
160 *
161 *   protected void validateEx() { }
162 *
163 *   public abstract void grow();
164 * }
165 * </pre>
166 * <ul>
167 * <li>
168 * Property {@code ignoredAnnotations} - Specify annotations which allow the check to
169 * skip the method from validation.
170 * Type is {@code java.lang.String[]}.
171 * Default value is {@code After, AfterClass, Before, BeforeClass, Test}.
172 * </li>
173 * <li>
174 * Property {@code requiredJavadocPhrase} - Specify the comment text pattern which qualifies a
175 * method as designed for extension. Supports multi-line regex.
176 * Type is {@code java.util.regex.Pattern}.
177 * Default value is {@code ".*"}.
178 * </li>
179 * </ul>
180 *
181 * <p>
182 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
183 * </p>
184 *
185 * <p>
186 * Violation Message Keys:
187 * </p>
188 * <ul>
189 * <li>
190 * {@code design.forExtension}
191 * </li>
192 * </ul>
193 *
194 * @since 3.1
195 */
196@StatelessCheck
197public class DesignForExtensionCheck extends AbstractCheck {
198
199    /**
200     * A key is pointing to the warning message text in "messages.properties"
201     * file.
202     */
203    public static final String MSG_KEY = "design.forExtension";
204
205    /**
206     * Specify annotations which allow the check to skip the method from validation.
207     */
208    private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
209        "BeforeClass", "AfterClass", }).collect(Collectors.toUnmodifiableSet());
210
211    /**
212     * Specify the comment text pattern which qualifies a method as designed for extension.
213     * Supports multi-line regex.
214     */
215    private Pattern requiredJavadocPhrase = Pattern.compile(".*");
216
217    /**
218     * Setter to specify annotations which allow the check to skip the method from validation.
219     *
220     * @param ignoredAnnotations method annotations.
221     * @since 7.2
222     */
223    public void setIgnoredAnnotations(String... ignoredAnnotations) {
224        this.ignoredAnnotations = Arrays.stream(ignoredAnnotations)
225            .collect(Collectors.toUnmodifiableSet());
226    }
227
228    /**
229     * Setter to specify the comment text pattern which qualifies a
230     * method as designed for extension. Supports multi-line regex.
231     *
232     * @param requiredJavadocPhrase method annotations.
233     * @since 8.40
234     */
235    public void setRequiredJavadocPhrase(Pattern requiredJavadocPhrase) {
236        this.requiredJavadocPhrase = requiredJavadocPhrase;
237    }
238
239    @Override
240    public int[] getDefaultTokens() {
241        return getRequiredTokens();
242    }
243
244    @Override
245    public int[] getAcceptableTokens() {
246        return getRequiredTokens();
247    }
248
249    @Override
250    public int[] getRequiredTokens() {
251        // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
252        // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
253        // stack to hold CLASS_DEF tokens.
254        return new int[] {TokenTypes.METHOD_DEF};
255    }
256
257    @Override
258    public boolean isCommentNodesRequired() {
259        return true;
260    }
261
262    @Override
263    public void visitToken(DetailAST ast) {
264        if (!hasJavadocComment(ast)
265                && canBeOverridden(ast)
266                && (isNativeMethod(ast)
267                    || !hasEmptyImplementation(ast))
268                && !hasIgnoredAnnotation(ast, ignoredAnnotations)
269                && !ScopeUtil.isInRecordBlock(ast)) {
270            final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
271            if (canBeSubclassed(classDef)) {
272                final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
273                final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
274                log(ast, MSG_KEY, className, methodName);
275            }
276        }
277    }
278
279    /**
280     * Checks whether a method has a javadoc comment.
281     *
282     * @param methodDef method definition token.
283     * @return true if a method has a javadoc comment.
284     */
285    private boolean hasJavadocComment(DetailAST methodDef) {
286        return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
287                || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
288    }
289
290    /**
291     * Checks whether a token has a javadoc comment.
292     *
293     * @param methodDef method definition token.
294     * @param tokenType token type.
295     * @return true if a token has a javadoc comment.
296     */
297    private boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) {
298        final DetailAST token = methodDef.findFirstToken(tokenType);
299        return branchContainsJavadocComment(token);
300    }
301
302    /**
303     * Checks whether a javadoc comment exists under the token.
304     *
305     * @param token tree token.
306     * @return true if a javadoc comment exists under the token.
307     */
308    private boolean branchContainsJavadocComment(DetailAST token) {
309        boolean result = false;
310        DetailAST curNode = token;
311        while (curNode != null) {
312            if (curNode.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
313                    && JavadocUtil.isJavadocComment(curNode)) {
314                result = hasValidJavadocComment(curNode);
315                break;
316            }
317
318            DetailAST toVisit = curNode.getFirstChild();
319            while (toVisit == null) {
320                if (curNode == token) {
321                    break;
322                }
323
324                toVisit = curNode.getNextSibling();
325                curNode = curNode.getParent();
326            }
327            curNode = toVisit;
328        }
329
330        return result;
331    }
332
333    /**
334     * Checks whether a javadoc contains the specified comment pattern that denotes
335     * a method as designed for extension.
336     *
337     * @param detailAST the ast we are checking for possible extension
338     * @return true if the javadoc of this ast contains the required comment pattern
339     */
340    private boolean hasValidJavadocComment(DetailAST detailAST) {
341        final String javadocString =
342            JavadocUtil.getBlockCommentContent(detailAST);
343
344        final Matcher requiredJavadocPhraseMatcher =
345            requiredJavadocPhrase.matcher(javadocString);
346
347        return requiredJavadocPhraseMatcher.find();
348    }
349
350    /**
351     * Checks whether a method is native.
352     *
353     * @param ast method definition token.
354     * @return true if a methods is native.
355     */
356    private static boolean isNativeMethod(DetailAST ast) {
357        final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
358        return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
359    }
360
361    /**
362     * Checks whether a method has only comments in the body (has an empty implementation).
363     * Method is OK if its implementation is empty.
364     *
365     * @param ast method definition token.
366     * @return true if a method has only comments in the body.
367     */
368    private static boolean hasEmptyImplementation(DetailAST ast) {
369        boolean hasEmptyBody = true;
370        final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
371        final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
372        final Predicate<DetailAST> predicate = currentNode -> {
373            return currentNode != methodImplCloseBrace
374                && !TokenUtil.isCommentType(currentNode.getType());
375        };
376        final Optional<DetailAST> methodBody =
377            TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
378        if (methodBody.isPresent()) {
379            hasEmptyBody = false;
380        }
381        return hasEmptyBody;
382    }
383
384    /**
385     * Checks whether a method can be overridden.
386     * Method can be overridden if it is not private, abstract, final or static.
387     * Note that the check has nothing to do for interfaces.
388     *
389     * @param methodDef method definition token.
390     * @return true if a method can be overridden in a subclass.
391     */
392    private static boolean canBeOverridden(DetailAST methodDef) {
393        final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
394        return ScopeUtil.getSurroundingScope(methodDef).isIn(Scope.PROTECTED)
395            && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef)
396            && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
397            && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null
398            && modifiers.findFirstToken(TokenTypes.FINAL) == null
399            && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null;
400    }
401
402    /**
403     * Checks whether a method has any of ignored annotations.
404     *
405     * @param methodDef method definition token.
406     * @param annotations a set of ignored annotations.
407     * @return true if a method has any of ignored annotations.
408     */
409    private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
410        final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
411        final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers,
412            currentToken -> {
413                return currentToken.getType() == TokenTypes.ANNOTATION
414                    && annotations.contains(getAnnotationName(currentToken));
415            });
416        return annotation.isPresent();
417    }
418
419    /**
420     * Gets the name of the annotation.
421     *
422     * @param annotation to get name of.
423     * @return the name of the annotation.
424     */
425    private static String getAnnotationName(DetailAST annotation) {
426        final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
427        final DetailAST parent = Objects.requireNonNullElse(dotAst, annotation);
428        return parent.findFirstToken(TokenTypes.IDENT).getText();
429    }
430
431    /**
432     * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
433     * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
434     *
435     * @param ast the start node for searching.
436     * @return the CLASS_DEF or ENUM_DEF token.
437     */
438    private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
439        DetailAST searchAST = ast;
440        while (searchAST.getType() != TokenTypes.CLASS_DEF
441               && searchAST.getType() != TokenTypes.ENUM_DEF) {
442            searchAST = searchAST.getParent();
443        }
444        return searchAST;
445    }
446
447    /**
448     * Checks if the given class (given CLASS_DEF node) can be subclassed.
449     *
450     * @param classDef class definition token.
451     * @return true if the containing class can be subclassed.
452     */
453    private static boolean canBeSubclassed(DetailAST classDef) {
454        final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
455        return classDef.getType() != TokenTypes.ENUM_DEF
456            && modifiers.findFirstToken(TokenTypes.FINAL) == null
457            && hasDefaultOrExplicitNonPrivateCtor(classDef);
458    }
459
460    /**
461     * Checks whether a class has default or explicit non-private constructor.
462     *
463     * @param classDef class ast token.
464     * @return true if a class has default or explicit non-private constructor.
465     */
466    private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
467        // check if subclassing is prevented by having only private ctors
468        final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
469
470        boolean hasDefaultConstructor = true;
471        boolean hasExplicitNonPrivateCtor = false;
472
473        DetailAST candidate = objBlock.getFirstChild();
474
475        while (candidate != null) {
476            if (candidate.getType() == TokenTypes.CTOR_DEF) {
477                hasDefaultConstructor = false;
478
479                final DetailAST ctorMods =
480                        candidate.findFirstToken(TokenTypes.MODIFIERS);
481                if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
482                    hasExplicitNonPrivateCtor = true;
483                    break;
484                }
485            }
486            candidate = candidate.getNextSibling();
487        }
488
489        return hasDefaultConstructor || hasExplicitNonPrivateCtor;
490    }
491
492}