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