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