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