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