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.imports;
21
22 import java.util.Collection;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Optional;
26 import java.util.Set;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import java.util.stream.Stream;
30
31 import org.checkerframework.checker.index.qual.IndexOrLow;
32
33 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
34 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
35 import com.puppycrawl.tools.checkstyle.api.DetailAST;
36 import com.puppycrawl.tools.checkstyle.api.FileContents;
37 import com.puppycrawl.tools.checkstyle.api.FullIdent;
38 import com.puppycrawl.tools.checkstyle.api.TextBlock;
39 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
40 import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
41 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
43 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 @FileStatefulCheck
115 public class UnusedImportsCheck extends AbstractCheck {
116
117
118
119
120
121 public static final String MSG_KEY = "import.unused";
122
123
124 private static final Pattern CLASS_NAME = CommonUtil.createPattern(
125 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
126
127 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
128 "^" + CLASS_NAME);
129
130 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
131 "[(,]\\s*" + CLASS_NAME.pattern());
132
133
134 private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
135 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
136
137
138 private static final Pattern REFERENCE = Pattern.compile(
139 "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$",
140 Pattern.CASE_INSENSITIVE
141 );
142
143
144 private static final Pattern METHOD = Pattern.compile(
145 "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$",
146 Pattern.CASE_INSENSITIVE
147 );
148
149
150 private static final String STAR_IMPORT_SUFFIX = ".*";
151
152
153 private final Set<FullIdent> imports = new HashSet<>();
154
155
156 private boolean collect;
157
158 private boolean processJavadoc = true;
159
160
161
162
163
164 private Frame currentFrame;
165
166
167
168
169
170
171
172 public void setProcessJavadoc(boolean value) {
173 processJavadoc = value;
174 }
175
176 @Override
177 public void beginTree(DetailAST rootAST) {
178 collect = false;
179 currentFrame = Frame.compilationUnit();
180 imports.clear();
181 }
182
183 @Override
184 public void finishTree(DetailAST rootAST) {
185 currentFrame.finish();
186
187 imports.stream()
188 .filter(imprt -> isUnusedImport(imprt.getText()))
189 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
190 }
191
192 @Override
193 public int[] getDefaultTokens() {
194 return getRequiredTokens();
195 }
196
197 @Override
198 public int[] getRequiredTokens() {
199 return new int[] {
200 TokenTypes.IDENT,
201 TokenTypes.IMPORT,
202 TokenTypes.STATIC_IMPORT,
203
204 TokenTypes.PACKAGE_DEF,
205 TokenTypes.ANNOTATION_DEF,
206 TokenTypes.ANNOTATION_FIELD_DEF,
207 TokenTypes.ENUM_DEF,
208 TokenTypes.ENUM_CONSTANT_DEF,
209 TokenTypes.CLASS_DEF,
210 TokenTypes.INTERFACE_DEF,
211 TokenTypes.METHOD_DEF,
212 TokenTypes.CTOR_DEF,
213 TokenTypes.VARIABLE_DEF,
214 TokenTypes.RECORD_DEF,
215 TokenTypes.COMPACT_CTOR_DEF,
216
217 TokenTypes.OBJBLOCK,
218 TokenTypes.SLIST,
219 };
220 }
221
222 @Override
223 public int[] getAcceptableTokens() {
224 return getRequiredTokens();
225 }
226
227 @Override
228 public void visitToken(DetailAST ast) {
229 switch (ast.getType()) {
230 case TokenTypes.IDENT -> {
231 if (collect) {
232 processIdent(ast);
233 }
234 }
235 case TokenTypes.IMPORT -> processImport(ast);
236 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
237 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
238 default -> {
239 collect = true;
240 if (processJavadoc) {
241 collectReferencesFromJavadoc(ast);
242 }
243 }
244 }
245 }
246
247 @Override
248 public void leaveToken(DetailAST ast) {
249 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
250 currentFrame = currentFrame.pop();
251 }
252 }
253
254
255
256
257
258
259
260 private boolean isUnusedImport(String imprt) {
261 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
262 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
263 || javaLangPackageMatcher.matches();
264 }
265
266
267
268
269
270
271 private void processIdent(DetailAST ast) {
272 final DetailAST parent = ast.getParent();
273 final int parentType = parent.getType();
274
275 final boolean isClassOrMethod = parentType == TokenTypes.DOT
276 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
277
278 if (TokenUtil.isTypeDeclaration(parentType)) {
279 currentFrame.addDeclaredType(ast.getText());
280 }
281 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
282 currentFrame.addReferencedType(ast.getText());
283 }
284 }
285
286
287
288
289
290
291
292 private static boolean isQualifiedIdentifier(DetailAST ast) {
293 final DetailAST parent = ast.getParent();
294 final int parentType = parent.getType();
295
296 final boolean isQualifiedIdent = parentType == TokenTypes.DOT
297 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
298 && ast.getNextSibling() != null;
299 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
300 && ast.getNextSibling() != null;
301 return isQualifiedIdent || isQualifiedIdentFromMethodRef;
302 }
303
304
305
306
307
308
309 private void processImport(DetailAST ast) {
310 final FullIdent name = FullIdent.createFullIdentBelow(ast);
311 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
312 imports.add(name);
313 }
314 }
315
316
317
318
319
320
321 private void processStaticImport(DetailAST ast) {
322 final FullIdent name =
323 FullIdent.createFullIdent(
324 ast.getFirstChild().getNextSibling());
325 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
326 imports.add(name);
327 }
328 }
329
330
331
332
333
334
335
336 @SuppressWarnings("deprecation")
337 private void collectReferencesFromJavadoc(DetailAST ast) {
338 final FileContents contents = getFileContents();
339 final int lineNo = ast.getLineNo();
340 final TextBlock textBlock = contents.getJavadocBefore(lineNo);
341 if (textBlock != null) {
342 currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
343 }
344 }
345
346
347
348
349
350
351
352
353 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
354
355 final List<JavadocTag> inlineTags = getTargetTags(textBlock,
356 JavadocUtil.JavadocTagType.INLINE);
357
358 final List<JavadocTag> blockTags = getTargetTags(textBlock,
359 JavadocUtil.JavadocTagType.BLOCK);
360 final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream())
361 .toList();
362
363 final Set<String> references = new HashSet<>();
364
365 targetTags.stream()
366 .filter(JavadocTag::canReferenceImports)
367 .forEach(tag -> references.addAll(processJavadocTag(tag)));
368 return references;
369 }
370
371
372
373
374
375
376
377
378
379
380 private static List<JavadocTag> getTargetTags(TextBlock cmt,
381 JavadocUtil.JavadocTagType javadocTagType) {
382 return JavadocUtil.getJavadocTags(cmt, javadocTagType)
383 .getValidTags()
384 .stream()
385 .filter(tag -> isMatchingTagType(tag, javadocTagType))
386 .map(UnusedImportsCheck::bestTryToMatchReference)
387 .flatMap(Optional::stream)
388 .toList();
389 }
390
391
392
393
394
395
396
397 private static Set<String> processJavadocTag(JavadocTag tag) {
398 final Set<String> references = new HashSet<>();
399 final String identifier = tag.getFirstArg();
400 for (Pattern pattern : new Pattern[]
401 {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
402 references.addAll(matchPattern(identifier, pattern));
403 }
404 return references;
405 }
406
407
408
409
410
411
412
413
414
415 private static Set<String> matchPattern(String identifier, Pattern pattern) {
416 final Set<String> references = new HashSet<>();
417 final Matcher matcher = pattern.matcher(identifier);
418 while (matcher.find()) {
419 references.add(topLevelType(matcher.group(1)));
420 }
421 return references;
422 }
423
424
425
426
427
428
429
430
431
432 private static String topLevelType(String type) {
433 final String topLevelType;
434 final int dotIndex = type.indexOf('.');
435 if (dotIndex == -1) {
436 topLevelType = type;
437 }
438 else {
439 topLevelType = type.substring(0, dotIndex);
440 }
441 return topLevelType;
442 }
443
444
445
446
447
448
449
450
451
452
453
454 private static boolean isMatchingTagType(JavadocTag tag,
455 JavadocUtil.JavadocTagType javadocTagType) {
456 final boolean isInlineTag = tag.isInlineTag();
457 final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK;
458
459 return isBlockTagType != isInlineTag;
460 }
461
462
463
464
465
466
467
468
469 public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) {
470 final String content = tag.getFirstArg();
471 final int referenceIndex = extractReferencePart(content);
472 Optional<JavadocTag> validTag = Optional.empty();
473
474 if (referenceIndex != -1) {
475 final String referenceString;
476 if (referenceIndex == 0) {
477 referenceString = content;
478 }
479 else {
480 referenceString = content.substring(0, referenceIndex);
481 }
482 final Matcher matcher = REFERENCE.matcher(referenceString);
483 if (matcher.matches()) {
484 final int methodIndex = 3;
485 final String methodPart = matcher.group(methodIndex);
486 final boolean isValid = methodPart == null
487 || METHOD.matcher(methodPart).matches();
488 if (isValid) {
489 validTag = Optional.of(tag);
490 }
491 }
492 }
493 return validTag;
494 }
495
496
497
498
499
500
501
502
503 private static @IndexOrLow("#1")int extractReferencePart(String input) {
504 int parenthesesCount = 0;
505 int firstSpaceOutsideParens = -1;
506 for (int index = 0; index < input.length(); index++) {
507 final char currentCharacter = input.charAt(index);
508
509 if (currentCharacter == '(') {
510 parenthesesCount++;
511 }
512 else if (currentCharacter == ')') {
513 parenthesesCount--;
514 }
515 else if (currentCharacter == ' ' && parenthesesCount == 0) {
516 firstSpaceOutsideParens = index;
517 break;
518 }
519 }
520
521 int methodIndex = -1;
522 if (parenthesesCount == 0) {
523 if (firstSpaceOutsideParens == -1) {
524 methodIndex = 0;
525 }
526 else {
527 methodIndex = firstSpaceOutsideParens;
528 }
529 }
530 return methodIndex;
531 }
532
533
534
535
536 private static final class Frame {
537
538
539 private final Frame parent;
540
541
542 private final Set<String> declaredTypes;
543
544
545 private final Set<String> referencedTypes;
546
547
548
549
550
551
552 private Frame(Frame parent) {
553 this.parent = parent;
554 declaredTypes = new HashSet<>();
555 referencedTypes = new HashSet<>();
556 }
557
558
559
560
561
562
563 public void addDeclaredType(String type) {
564 declaredTypes.add(type);
565 }
566
567
568
569
570
571
572 public void addReferencedType(String type) {
573 referencedTypes.add(type);
574 }
575
576
577
578
579
580
581 public void addReferencedTypes(Collection<String> types) {
582 referencedTypes.addAll(types);
583 }
584
585
586
587
588
589 public void finish() {
590 referencedTypes.removeAll(declaredTypes);
591 }
592
593
594
595
596
597
598 public Frame push() {
599 return new Frame(this);
600 }
601
602
603
604
605
606
607 public Frame pop() {
608 finish();
609 parent.addReferencedTypes(referencedTypes);
610 return parent;
611 }
612
613
614
615
616
617
618
619 public boolean isReferencedType(String type) {
620 return referencedTypes.contains(type);
621 }
622
623
624
625
626
627
628 public static Frame compilationUnit() {
629 return new Frame(null);
630 }
631
632 }
633
634 }