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(Pattern... from) {
160 excludeClassesRegexps.addAll(Arrays.asList(from));
161 }
162
163
164
165
166
167
168
169 public final void setExcludedPackages(String... excludedPackages) {
170 final List<String> invalidIdentifiers = Arrays.stream(excludedPackages)
171 .filter(Predicate.not(CommonUtil::isName))
172 .collect(Collectors.toUnmodifiableList());
173 if (!invalidIdentifiers.isEmpty()) {
174 throw new IllegalArgumentException(
175 "the following values are not valid identifiers: " + invalidIdentifiers);
176 }
177
178 this.excludedPackages = Set.of(excludedPackages);
179 }
180
181 @Override
182 public final void beginTree(DetailAST ast) {
183 importedClassPackages.clear();
184 classesContexts.clear();
185 classesContexts.push(new ClassContext("", null));
186 packageName = "";
187 }
188
189 @Override
190 public void visitToken(DetailAST ast) {
191 switch (ast.getType()) {
192 case TokenTypes.PACKAGE_DEF:
193 visitPackageDef(ast);
194 break;
195 case TokenTypes.IMPORT:
196 registerImport(ast);
197 break;
198 case TokenTypes.CLASS_DEF:
199 case TokenTypes.INTERFACE_DEF:
200 case TokenTypes.ANNOTATION_DEF:
201 case TokenTypes.ENUM_DEF:
202 case TokenTypes.RECORD_DEF:
203 visitClassDef(ast);
204 break;
205 case TokenTypes.EXTENDS_CLAUSE:
206 case TokenTypes.IMPLEMENTS_CLAUSE:
207 case TokenTypes.TYPE:
208 visitType(ast);
209 break;
210 case TokenTypes.LITERAL_NEW:
211 visitLiteralNew(ast);
212 break;
213 case TokenTypes.LITERAL_THROWS:
214 visitLiteralThrows(ast);
215 break;
216 case TokenTypes.ANNOTATION:
217 visitAnnotationType(ast);
218 break;
219 default:
220 throw new IllegalArgumentException("Unknown type: " + ast);
221 }
222 }
223
224 @Override
225 public void leaveToken(DetailAST ast) {
226 if (TokenUtil.isTypeDeclaration(ast.getType())) {
227 leaveClassDef();
228 }
229 }
230
231
232
233
234
235
236 private void visitPackageDef(DetailAST pkg) {
237 final FullIdent ident = FullIdent.createFullIdent(pkg.getLastChild().getPreviousSibling());
238 packageName = ident.getText();
239 }
240
241
242
243
244
245
246 private void visitClassDef(DetailAST classDef) {
247 final String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
248 createNewClassContext(className, classDef);
249 }
250
251
252 private void leaveClassDef() {
253 checkCurrentClassAndRestorePrevious();
254 }
255
256
257
258
259
260
261 private void registerImport(DetailAST imp) {
262 final FullIdent ident = FullIdent.createFullIdent(
263 imp.getLastChild().getPreviousSibling());
264 final String fullName = ident.getText();
265 final int lastDot = fullName.lastIndexOf(DOT);
266 importedClassPackages.put(fullName.substring(lastDot + 1), fullName);
267 }
268
269
270
271
272
273
274
275 private void createNewClassContext(String className, DetailAST ast) {
276 classesContexts.push(new ClassContext(className, ast));
277 }
278
279
280 private void checkCurrentClassAndRestorePrevious() {
281 classesContexts.pop().checkCoupling();
282 }
283
284
285
286
287
288
289 private void visitType(DetailAST ast) {
290 classesContexts.peek().visitType(ast);
291 }
292
293
294
295
296
297
298 private void visitLiteralNew(DetailAST ast) {
299 classesContexts.peek().visitLiteralNew(ast);
300 }
301
302
303
304
305
306
307 private void visitLiteralThrows(DetailAST ast) {
308 classesContexts.peek().visitLiteralThrows(ast);
309 }
310
311
312
313
314
315
316 private void visitAnnotationType(DetailAST annotationAST) {
317 final DetailAST children = annotationAST.getFirstChild();
318 final DetailAST type = children.getNextSibling();
319 classesContexts.peek().addReferencedClassName(type.getText());
320 }
321
322
323
324
325
326 private final class ClassContext {
327
328
329
330
331
332 private final Set<String> referencedClassNames = new TreeSet<>();
333
334 private final String className;
335
336
337 private final DetailAST classAst;
338
339
340
341
342
343
344
345 private ClassContext(String className, DetailAST ast) {
346 this.className = className;
347 classAst = ast;
348 }
349
350
351
352
353
354
355 public void visitLiteralThrows(DetailAST literalThrows) {
356 for (DetailAST childAST = literalThrows.getFirstChild();
357 childAST != null;
358 childAST = childAST.getNextSibling()) {
359 if (childAST.getType() != TokenTypes.COMMA) {
360 addReferencedClassName(childAST);
361 }
362 }
363 }
364
365
366
367
368
369
370 public void visitType(DetailAST ast) {
371 DetailAST child = ast.getFirstChild();
372 while (child != null) {
373 if (TokenUtil.isOfType(child, TokenTypes.IDENT, TokenTypes.DOT)) {
374 final String fullTypeName = FullIdent.createFullIdent(child).getText();
375 final String trimmed = BRACKET_PATTERN
376 .matcher(fullTypeName).replaceAll("");
377 addReferencedClassName(trimmed);
378 }
379 child = child.getNextSibling();
380 }
381 }
382
383
384
385
386
387
388 public void visitLiteralNew(DetailAST ast) {
389 addReferencedClassName(ast.getFirstChild());
390 }
391
392
393
394
395
396
397 private void addReferencedClassName(DetailAST ast) {
398 final String fullIdentName = FullIdent.createFullIdent(ast).getText();
399 final String trimmed = BRACKET_PATTERN
400 .matcher(fullIdentName).replaceAll("");
401 addReferencedClassName(trimmed);
402 }
403
404
405
406
407
408
409 private void addReferencedClassName(String referencedClassName) {
410 if (isSignificant(referencedClassName)) {
411 referencedClassNames.add(referencedClassName);
412 }
413 }
414
415
416 public void checkCoupling() {
417 referencedClassNames.remove(className);
418 referencedClassNames.remove(packageName + DOT + className);
419
420 if (referencedClassNames.size() > max) {
421 log(classAst, getLogMessageId(),
422 referencedClassNames.size(), max,
423 referencedClassNames.toString());
424 }
425 }
426
427
428
429
430
431
432
433 private boolean isSignificant(String candidateClassName) {
434 return !excludedClasses.contains(candidateClassName)
435 && !isFromExcludedPackage(candidateClassName)
436 && !isExcludedClassRegexp(candidateClassName);
437 }
438
439
440
441
442
443
444
445 private boolean isFromExcludedPackage(String candidateClassName) {
446 String classNameWithPackage = candidateClassName;
447 if (candidateClassName.indexOf(DOT) == -1) {
448 classNameWithPackage = getClassNameWithPackage(candidateClassName)
449 .orElse("");
450 }
451 boolean isFromExcludedPackage = false;
452 if (classNameWithPackage.indexOf(DOT) != -1) {
453 final int lastDotIndex = classNameWithPackage.lastIndexOf(DOT);
454 final String candidatePackageName =
455 classNameWithPackage.substring(0, lastDotIndex);
456 isFromExcludedPackage = candidatePackageName.startsWith("java.lang")
457 || excludedPackages.contains(candidatePackageName);
458 }
459 return isFromExcludedPackage;
460 }
461
462
463
464
465
466
467
468
469 private Optional<String> getClassNameWithPackage(String examineClassName) {
470 return Optional.ofNullable(importedClassPackages.get(examineClassName));
471 }
472
473
474
475
476
477
478
479 private boolean isExcludedClassRegexp(String candidateClassName) {
480 boolean result = false;
481 for (Pattern pattern : excludeClassesRegexps) {
482 if (pattern.matcher(candidateClassName).matches()) {
483 result = true;
484 break;
485 }
486 }
487 return result;
488 }
489
490 }
491
492 }