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