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