1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 @StatelessCheck
93 public class DesignForExtensionCheck extends AbstractCheck {
94
95
96
97
98
99 public static final String MSG_KEY = "design.forExtension";
100
101
102
103
104 private Set<String> ignoredAnnotations = Arrays.stream(new String[] {"Test", "Before", "After",
105 "BeforeClass", "AfterClass", }).collect(Collectors.toSet());
106
107
108
109
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
128
129
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
156
157
158
159 private static boolean hasJavadocComment(DetailAST methodDef) {
160 return hasJavadocCommentOnToken(methodDef, TokenTypes.MODIFIERS)
161 || hasJavadocCommentOnToken(methodDef, TokenTypes.TYPE);
162 }
163
164
165
166
167
168
169
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
178
179
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
188
189
190
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
210
211
212
213
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
227
228
229
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
243
244
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
260
261
262
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
275
276
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
287
288
289
290 private static boolean hasDefaultOrExplicitNonPrivateCtor(DetailAST classDef) {
291
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 }