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