1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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.Objects;
24 import java.util.Optional;
25 import java.util.Set;
26 import java.util.function.Predicate;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import java.util.stream.Collectors;
30
31 import com.puppycrawl.tools.checkstyle.StatelessCheck;
32 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
33 import com.puppycrawl.tools.checkstyle.api.DetailAST;
34 import com.puppycrawl.tools.checkstyle.api.Scope;
35 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
37 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
38 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
39
40 /**
41 * <div>
42 * Checks that classes are designed for extension (subclass creation).
43 * </div>
44 *
45 * <p>
46 * Nothing wrong could be with founded classes.
47 * This check makes sense only for library projects (not application projects)
48 * which care of ideal OOP-design to make sure that class works in all cases even misusage.
49 * Even in library projects this check most likely will find classes that are designed for extension
50 * by somebody. User needs to use suppressions extensively to got a benefit from this check,
51 * and keep in suppressions all confirmed/known classes that are deigned for inheritance
52 * intentionally to let the check catch only new classes, and bring this to team/user attention.
53 * </p>
54 *
55 * <p>
56 * ATTENTION: Only user can decide whether a class is designed for extension or not.
57 * The check just shows all classes which are possibly designed for extension.
58 * If something inappropriate is found please use suppression.
59 * </p>
60 *
61 * <p>
62 * ATTENTION: If the method which can be overridden in a subclass has a javadoc comment
63 * (a good practice is to explain its self-use of overridable methods) the check will not
64 * rise a violation. The violation can also be skipped if the method which can be overridden
65 * in a subclass has one or more annotations that are specified in ignoredAnnotations
66 * option. Note, that by default @Override annotation is not included in the
67 * ignoredAnnotations set as in a subclass the method which has the annotation can also be
68 * overridden in its subclass.
69 * </p>
70 *
71 * <p>
72 * Problem is described at "Effective Java, 2nd Edition by Joshua Bloch" book, chapter
73 * "Item 17: Design and document for inheritance or else prohibit it".
74 * </p>
75 *
76 * <p>
77 * Some quotes from book:
78 * </p>
79 * <blockquote>The class must document its self-use of overridable methods.
80 * By convention, a method that invokes overridable methods contains a description
81 * of these invocations at the end of its documentation comment. The description
82 * begins with the phrase “This implementation.”
83 * </blockquote>
84 * <blockquote>
85 * The best solution to this problem is to prohibit subclassing in classes that
86 * are not designed and documented to be safely subclassed.
87 * </blockquote>
88 * <blockquote>
89 * If a concrete class does not implement a standard interface, then you may
90 * inconvenience some programmers by prohibiting inheritance. If you feel that you
91 * must allow inheritance from such a class, one reasonable approach is to ensure
92 * that the class never invokes any of its overridable methods and to document this
93 * fact. In other words, eliminate the class’s self-use of overridable methods entirely.
94 * In doing so, you’ll create a class that is reasonably safe to subclass. Overriding a
95 * method will never affect the behavior of any other method.
96 * </blockquote>
97 *
98 * <p>
99 * 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 * {@snippet lang="text" :
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 * @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 * }
146 *
147 * <p>
148 * Example of code without violation:
149 * </p>
150 * {@snippet lang="text" :
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 * }
166 *
167 * @since 3.1
168 */
169 @StatelessCheck
170 public class DesignForExtensionCheck extends AbstractCheck {
171
172 /**
173 * A key is pointing to the warning message text in "messages.properties"
174 * file.
175 */
176 public static final String MSG_KEY = "design.forExtension";
177
178 /**
179 * Specify annotations which allow the check to skip the method from validation.
180 */
181 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
182 "BeforeClass", "AfterClass", }).collect(Collectors.toUnmodifiableSet());
183
184 /**
185 * Specify the comment text pattern which qualifies a method as designed for extension.
186 * Supports multi-line regex.
187 */
188 private Pattern requiredJavadocPhrase = Pattern.compile(".*");
189
190 /**
191 * Creates a new {@code DesignForExtensionCheck} instance.
192 */
193 public DesignForExtensionCheck() {
194 // no code by default
195 }
196
197 /**
198 * Setter to specify annotations which allow the check to skip the method from validation.
199 *
200 * @param ignoredAnnotations method annotations.
201 * @since 7.2
202 */
203 public void setIgnoredAnnotations(String... ignoredAnnotations) {
204 this.ignoredAnnotations = Arrays.stream(ignoredAnnotations)
205 .collect(Collectors.toUnmodifiableSet());
206 }
207
208 /**
209 * Setter to specify the comment text pattern which qualifies a
210 * method as designed for extension. Supports multi-line regex.
211 *
212 * @param requiredJavadocPhrase method annotations.
213 * @since 8.40
214 */
215 public void setRequiredJavadocPhrase(Pattern requiredJavadocPhrase) {
216 this.requiredJavadocPhrase = requiredJavadocPhrase;
217 }
218
219 @Override
220 public int[] getDefaultTokens() {
221 return getRequiredTokens();
222 }
223
224 @Override
225 public int[] getAcceptableTokens() {
226 return getRequiredTokens();
227 }
228
229 @Override
230 public int[] getRequiredTokens() {
231 // The check does not subscribe to CLASS_DEF token as now it is stateless. If the check
232 // subscribes to CLASS_DEF token it will become stateful, since we need to have additional
233 // stack to hold CLASS_DEF tokens.
234 return new int[] {TokenTypes.METHOD_DEF};
235 }
236
237 @Override
238 public boolean isCommentNodesRequired() {
239 return true;
240 }
241
242 @Override
243 public void visitToken(DetailAST ast) {
244 if (!hasJavadocComment(ast)
245 && canBeOverridden(ast)
246 && (isNativeMethod(ast)
247 || !hasEmptyImplementation(ast))
248 && !hasIgnoredAnnotation(ast, ignoredAnnotations)
249 && !ScopeUtil.isInRecordBlock(ast)) {
250 final DetailAST classDef = getNearestClassOrEnumDefinition(ast);
251 if (canBeSubclassed(classDef)) {
252 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
253 final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
254 log(ast, MSG_KEY, className, methodName);
255 }
256 }
257 }
258
259 /**
260 * Checks whether a method has a javadoc comment.
261 *
262 * @param methodDef method definition token.
263 * @return true if a method has a javadoc comment.
264 */
265 private boolean hasJavadocComment(DetailAST methodDef) {
266 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
267 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
268 }
269
270 /**
271 * Checks whether a token has a javadoc comment.
272 *
273 * @param methodDef method definition token.
274 * @param tokenType token type.
275 * @return true if a token has a javadoc comment.
276 */
277 private boolean hasJavadocCommentOnToken(DetailAST methodDef, int tokenType) {
278 final DetailAST token = methodDef.findFirstToken(tokenType);
279 return branchContainsJavadocComment(token);
280 }
281
282 /**
283 * Checks whether a javadoc comment exists under the token.
284 *
285 * @param token tree token.
286 * @return true if a javadoc comment exists under the token.
287 */
288 private boolean branchContainsJavadocComment(DetailAST token) {
289 boolean result = false;
290 DetailAST curNode = token;
291 while (curNode != null) {
292 if (curNode.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
293 && JavadocUtil.isJavadocComment(curNode)) {
294 result = hasValidJavadocComment(curNode);
295 break;
296 }
297
298 DetailAST toVisit = curNode.getFirstChild();
299 while (toVisit == null) {
300 if (curNode == token) {
301 break;
302 }
303
304 toVisit = curNode.getNextSibling();
305 curNode = curNode.getParent();
306 }
307 curNode = toVisit;
308 }
309
310 return result;
311 }
312
313 /**
314 * Checks whether a javadoc contains the specified comment pattern that denotes
315 * a method as designed for extension.
316 *
317 * @param detailAST the ast we are checking for possible extension
318 * @return true if the javadoc of this ast contains the required comment pattern
319 */
320 private boolean hasValidJavadocComment(DetailAST detailAST) {
321 final String javadocString =
322 JavadocUtil.getBlockCommentContent(detailAST);
323
324 final Matcher requiredJavadocPhraseMatcher =
325 requiredJavadocPhrase.matcher(javadocString);
326
327 return requiredJavadocPhraseMatcher.find();
328 }
329
330 /**
331 * Checks whether a method is native.
332 *
333 * @param ast method definition token.
334 * @return true if a methods is native.
335 */
336 private static boolean isNativeMethod(DetailAST ast) {
337 final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
338 return mods.findFirstToken(TokenTypes.LITERAL_NATIVE) != null;
339 }
340
341 /**
342 * Checks whether a method has only comments in the body (has an empty implementation).
343 * Method is OK if its implementation is empty.
344 *
345 * @param ast method definition token.
346 * @return true if a method has only comments in the body.
347 */
348 private static boolean hasEmptyImplementation(DetailAST ast) {
349 boolean hasEmptyBody = true;
350 final DetailAST methodImplOpenBrace = ast.findFirstToken(TokenTypes.SLIST);
351 final DetailAST methodImplCloseBrace = methodImplOpenBrace.getLastChild();
352 final Predicate<DetailAST> predicate = currentNode -> {
353 return currentNode != methodImplCloseBrace
354 && !TokenUtil.isCommentType(currentNode.getType());
355 };
356 final Optional<DetailAST> methodBody =
357 TokenUtil.findFirstTokenByPredicate(methodImplOpenBrace, predicate);
358 if (methodBody.isPresent()) {
359 hasEmptyBody = false;
360 }
361 return hasEmptyBody;
362 }
363
364 /**
365 * Checks whether a method can be overridden.
366 * Method can be overridden if it is not private, abstract, final or static.
367 * Note that the check has nothing to do for interfaces.
368 *
369 * @param methodDef method definition token.
370 * @return true if a method can be overridden in a subclass.
371 */
372 private static boolean canBeOverridden(DetailAST methodDef) {
373 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
374 return ScopeUtil.getSurroundingScope(methodDef)
375 .map(surroundingScope -> {
376 return surroundingScope.isIn(Scope.PROTECTED)
377 && !ScopeUtil.isInInterfaceOrAnnotationBlock(methodDef)
378 && modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
379 && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null
380 && modifiers.findFirstToken(TokenTypes.FINAL) == null
381 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null;
382 })
383 .orElse(Boolean.FALSE);
384 }
385
386 /**
387 * Checks whether a method has any of ignored annotations.
388 *
389 * @param methodDef method definition token.
390 * @param annotations a set of ignored annotations.
391 * @return true if a method has any of ignored annotations.
392 */
393 private static boolean hasIgnoredAnnotation(DetailAST methodDef, Set<String> annotations) {
394 final DetailAST modifiers = methodDef.findFirstToken(TokenTypes.MODIFIERS);
395 final Optional<DetailAST> annotation = TokenUtil.findFirstTokenByPredicate(modifiers,
396 currentToken -> {
397 return currentToken.getType() == TokenTypes.ANNOTATION
398 && annotations.contains(getAnnotationName(currentToken));
399 });
400 return annotation.isPresent();
401 }
402
403 /**
404 * Gets the name of the annotation.
405 *
406 * @param annotation to get name of.
407 * @return the name of the annotation.
408 */
409 private static String getAnnotationName(DetailAST annotation) {
410 final DetailAST dotAst = annotation.findFirstToken(TokenTypes.DOT);
411 final DetailAST parent = Objects.requireNonNullElse(dotAst, annotation);
412 return parent.findFirstToken(TokenTypes.IDENT).getText();
413 }
414
415 /**
416 * Returns CLASS_DEF or ENUM_DEF token which is the nearest to the given ast node.
417 * Searches the tree towards the root until it finds a CLASS_DEF or ENUM_DEF node.
418 *
419 * @param ast the start node for searching.
420 * @return the CLASS_DEF or ENUM_DEF token.
421 */
422 private static DetailAST getNearestClassOrEnumDefinition(DetailAST ast) {
423 DetailAST searchAST = ast;
424 while (searchAST.getType() != TokenTypes.CLASS_DEF
425 && searchAST.getType() != TokenTypes.ENUM_DEF) {
426 searchAST = searchAST.getParent();
427 }
428 return searchAST;
429 }
430
431 /**
432 * Checks if the given class (given CLASS_DEF node) can be subclassed.
433 *
434 * @param classDef class definition token.
435 * @return true if the containing class can be subclassed.
436 */
437 private static boolean canBeSubclassed(DetailAST classDef) {
438 final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
439 return classDef.getType() != TokenTypes.ENUM_DEF
440 && modifiers.findFirstToken(TokenTypes.FINAL) == null
441 && hasDefaultOrExplicitNonPrivateCtor(classDef);
442 }
443
444 /**
445 * Checks whether a class has default or explicit non-private constructor.
446 *
447 * @param classDef class ast token.
448 * @return true if a class has default or explicit non-private constructor.
449 */
450 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
451 // check if subclassing is prevented by having only private ctors
452 final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
453
454 boolean hasDefaultConstructor = true;
455 boolean hasExplicitNonPrivateCtor = false;
456
457 DetailAST candidate = objBlock.getFirstChild();
458
459 while (candidate != null) {
460 if (candidate.getType() == TokenTypes.CTOR_DEF) {
461 hasDefaultConstructor = false;
462
463 final DetailAST ctorMods =
464 candidate.findFirstToken(TokenTypes.MODIFIERS);
465 if (ctorMods.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
466 hasExplicitNonPrivateCtor = true;
467 break;
468 }
469 }
470 candidate = candidate.getNextSibling();
471 }
472
473 return hasDefaultConstructor || hasExplicitNonPrivateCtor;
474 }
475
476 }