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.metrics;
21
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.Deque;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.Set;
32 import java.util.TreeSet;
33 import java.util.regex.Pattern;
34 import java.util.stream.Collectors;
35
36 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
37 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
38 import com.puppycrawl.tools.checkstyle.api.DetailAST;
39 import com.puppycrawl.tools.checkstyle.api.FullIdent;
40 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
41 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
42 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43
44
45
46
47
48 @FileStatefulCheck
49 public abstract class AbstractClassCouplingCheck extends AbstractCheck {
50
51
52 private static final String DOT = ".";
53
54
55 private static final Set<String> DEFAULT_EXCLUDED_CLASSES = Collections.unmodifiableSet(
56 Arrays.stream(new String[] {
57
58 "boolean", "byte", "char", "double", "float", "int",
59 "long", "short", "void",
60
61 "Boolean", "Byte", "Character", "Double", "Float",
62 "Integer", "Long", "Short", "Void",
63
64 "Object", "Class",
65 "String", "StringBuffer", "StringBuilder",
66
67 "ArrayIndexOutOfBoundsException", "Exception",
68 "RuntimeException", "IllegalArgumentException",
69 "IllegalStateException", "IndexOutOfBoundsException",
70 "NullPointerException", "Throwable", "SecurityException",
71 "UnsupportedOperationException",
72
73 "List", "ArrayList", "Deque", "Queue", "LinkedList",
74 "Set", "HashSet", "SortedSet", "TreeSet",
75 "Map", "HashMap", "SortedMap", "TreeMap",
76 "Override", "Deprecated", "SafeVarargs", "SuppressWarnings", "FunctionalInterface",
77 }).collect(Collectors.toSet()));
78
79
80 private static final Set<String> DEFAULT_EXCLUDED_PACKAGES = Collections.emptySet();
81
82
83 private final List<Pattern> excludeClassesRegexps = new ArrayList<>();
84
85
86 private final Map<String, String> importedClassPackages = new HashMap<>();
87
88
89 private final Deque<ClassContext> classesContexts = new ArrayDeque<>();
90
91
92 private Set<String> excludedClasses = DEFAULT_EXCLUDED_CLASSES;
93
94 private Set<String> excludedPackages = DEFAULT_EXCLUDED_PACKAGES;
95
96 private int max;
97
98
99 private String packageName;
100
101
102
103
104
105 protected AbstractClassCouplingCheck(int defaultMax) {
106 max = defaultMax;
107 excludeClassesRegexps.add(CommonUtil.createPattern("^$"));
108 }
109
110
111
112
113
114 protected abstract String getLogMessageId();
115
116 @Override
117 public final int[] getDefaultTokens() {
118 return getRequiredTokens();
119 }
120
121
122
123
124
125 public final void setMax(int max) {
126 this.max = max;
127 }
128
129
130
131
132
133 public final void setExcludedClasses(String... excludedClasses) {
134 this.excludedClasses =
135 Collections.unmodifiableSet(Arrays.stream(excludedClasses).collect(Collectors.toSet()));
136 }
137
138
139
140
141
142 public void setExcludeClassesRegexps(String... from) {
143 excludeClassesRegexps.addAll(Arrays.stream(from.clone())
144 .map(CommonUtil::createPattern)
145 .collect(Collectors.toSet()));
146 }
147
148
149
150
151
152
153 public final void setExcludedPackages(String... excludedPackages) {
154 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
155 .filter(excludedPackageName -> !CommonUtil.isName(excludedPackageName))
156 .collect(Collectors.toList());
157 if (!invalidIdentifiers.isEmpty()) {
158 throw new IllegalArgumentException(
159 "the following values are not valid identifiers: "
160 + invalidIdentifiers.stream().collect(Collectors.joining(", ", "[", "]")));
161 }
162
163 this.excludedPackages = Collections.unmodifiableSet(
164 Arrays.stream(excludedPackages).collect(Collectors.toSet()));
165 }
166
167 @Override
168 public final void beginTree(DetailAST ast) {
169 importedClassPackages.clear();
170 classesContexts.clear();
171 classesContexts.push(new ClassContext("", null));
172 packageName = "";
173 }
174
175 @Override
176 public void visitToken(DetailAST ast) {
177 switch (ast.getType()) {
178 case TokenTypes.PACKAGE_DEF:
179 visitPackageDef(ast);
180 break;
181 case TokenTypes.IMPORT:
182 registerImport(ast);
183 break;
184 case TokenTypes.CLASS_DEF:
185 case TokenTypes.INTERFACE_DEF:
186 case TokenTypes.ANNOTATION_DEF:
187 case TokenTypes.ENUM_DEF:
188 visitClassDef(ast);
189 break;
190 case TokenTypes.EXTENDS_CLAUSE:
191 case TokenTypes.IMPLEMENTS_CLAUSE:
192 case TokenTypes.TYPE:
193 visitType(ast);
194 break;
195 case TokenTypes.LITERAL_NEW:
196 visitLiteralNew(ast);
197 break;
198 case TokenTypes.LITERAL_THROWS:
199 visitLiteralThrows(ast);
200 break;
201 case TokenTypes.ANNOTATION:
202 visitAnnotationType(ast);
203 break;
204 default:
205 throw new IllegalArgumentException("Unknown type: " + ast);
206 }
207 }
208
209 @Override
210 public void leaveToken(DetailAST ast) {
211 switch (ast.getType()) {
212 case TokenTypes.CLASS_DEF:
213 case TokenTypes.INTERFACE_DEF:
214 case TokenTypes.ANNOTATION_DEF:
215 case TokenTypes.ENUM_DEF:
216 leaveClassDef();
217 break;
218 default:
219
220 }
221 }
222
223
224
225
226
227 private void visitPackageDef(DetailAST pkg) {
228 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
229 packageName = ident.getText();
230 }
231
232
233
234
235
236 private void visitClassDef(DetailAST classDef) {
237 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
238 createNewClassContext(className, classDef);
239 }
240
241
242 private void leaveClassDef() {
243 checkCurrentClassAndRestorePrevious();
244 }
245
246
247
248
249
250 private void registerImport(DetailAST imp) {
251 final FullIdent ident = FullIdent.createFullIdent(
252 imp.getLastChild().getPreviousSibling());
253 final String fullName = ident.getText();
254 final int lastDot = fullName.lastIndexOf(DOT);
255 importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
256 }
257
258
259
260
261
262
263 private void createNewClassContext(String className, DetailAST ast) {
264 classesContexts.push(new ClassContext(className, ast));
265 }
266
267
268 private void checkCurrentClassAndRestorePrevious() {
269 classesContexts.pop().checkCoupling();
270 }
271
272
273
274
275
276 private void visitType(DetailAST ast) {
277 classesContexts.peek().visitType(ast);
278 }
279
280
281
282
283
284 private void visitLiteralNew(DetailAST ast) {
285 classesContexts.peek().visitLiteralNew(ast);
286 }
287
288
289
290
291
292 private void visitLiteralThrows(DetailAST ast) {
293 classesContexts.peek().visitLiteralThrows(ast);
294 }
295
296
297
298
299
300 private void visitAnnotationType(DetailAST annotationAST) {
301 final DetailAST children = annotationAST.getFirstChild();
302 final DetailAST type = children.getNextSibling();
303 classesContexts.peek().addReferencedClassName(type.getText());
304 }
305
306
307
308
309
310 private class ClassContext {
311
312
313
314
315
316 private final Set<String> referencedClassNames = new TreeSet<>();
317
318 private final String className;
319
320
321 private final DetailAST classAst;
322
323
324
325
326
327
328 ClassContext(String className, DetailAST ast) {
329 this.className = className;
330 classAst = ast;
331 }
332
333
334
335
336
337 public void visitLiteralThrows(DetailAST literalThrows) {
338 for (DetailAST childAST = literalThrows.getFirstChild();
339 childAST != null;
340 childAST = childAST.getNextSibling()) {
341 if (childAST.getType() != TokenTypes.COMMA) {
342 addReferencedClassName(childAST);
343 }
344 }
345 }
346
347
348
349
350
351 public void visitType(DetailAST ast) {
352 final String fullTypeName = CheckUtil.createFullType(ast).getText();
353 addReferencedClassName(fullTypeName);
354 }
355
356
357
358
359
360 public void visitLiteralNew(DetailAST ast) {
361 addReferencedClassName(ast.getFirstChild());
362 }
363
364
365
366
367
368 private void addReferencedClassName(DetailAST ast) {
369 final String fullIdentName = FullIdent.createFullIdent(ast).getText();
370 addReferencedClassName(fullIdentName);
371 }
372
373
374
375
376
377 private void addReferencedClassName(String referencedClassName) {
378 if (isSignificant(referencedClassName)) {
379 referencedClassNames.add(referencedClassName);
380 }
381 }
382
383
384 public void checkCoupling() {
385 referencedClassNames.remove(className);
386 referencedClassNames.remove(packageName + DOT + className);
387
388 if (referencedClassNames.size() > max) {
389 log(classAst, getLogMessageId(),
390 referencedClassNames.size(), max,
391 referencedClassNames.toString());
392 }
393 }
394
395
396
397
398
399
400 private boolean isSignificant(String candidateClassName) {
401 return !excludedClasses.contains(candidateClassName)
402 && !isFromExcludedPackage(candidateClassName)
403 && !isExcludedClassRegexp(candidateClassName);
404 }
405
406
407
408
409
410
411 private boolean isFromExcludedPackage(String candidateClassName) {
412 String classNameWithPackage = candidateClassName;
413 if (!candidateClassName.contains(DOT)) {
414 classNameWithPackage = getClassNameWithPackage(candidateClassName)
415 .orElse("");
416 }
417 boolean isFromExcludedPackage = false;
418 if (classNameWithPackage.contains(DOT)) {
419 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
420 final String candidatePackageName =
421 classNameWithPackage.substring(0, lastDotIndex);
422 isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
423 || excludedPackages.contains(candidatePackageName);
424 }
425 return isFromExcludedPackage;
426 }
427
428
429
430
431
432
433
434 private Optional<String> getClassNameWithPackage(String examineClassName) {
435 return Optional.ofNullable(importedClassPackages.get(examineClassName));
436 }
437
438
439
440
441
442
443 private boolean isExcludedClassRegexp(String candidateClassName) {
444 boolean result = false;
445 for (Pattern pattern : excludeClassesRegexps) {
446 if (pattern.matcher(candidateClassName).matches()) {
447 result = true;
448 break;
449 }
450 }
451 return result;
452 }
453
454 }
455
456 }