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.ArrayList;
23 import java.util.Collection;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.FileContents;
34 import com.puppycrawl.tools.checkstyle.api.FullIdent;
35 import com.puppycrawl.tools.checkstyle.api.TextBlock;
36 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37 import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
38 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
40 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
41
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111 @FileStatefulCheck
112 public class UnusedImportsCheck extends AbstractCheck {
113
114
115
116
117
118 public static final String MSG_KEY = "import.unused";
119
120
121 private static final Pattern CLASS_NAME = CommonUtil.createPattern(
122 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
123
124 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
125 "^" + CLASS_NAME);
126
127 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
128 "[(,]\\s*" + CLASS_NAME.pattern());
129
130
131 private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
132 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
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 break;
220 case TokenTypes.IMPORT:
221 processImport(ast);
222 break;
223 case TokenTypes.STATIC_IMPORT:
224 processStaticImport(ast);
225 break;
226 case TokenTypes.OBJBLOCK:
227 case TokenTypes.SLIST:
228 currentFrame = currentFrame.push();
229 break;
230 default:
231 collect = true;
232 if (processJavadoc) {
233 collectReferencesFromJavadoc(ast);
234 }
235 break;
236 }
237 }
238
239 @Override
240 public void leaveToken(DetailAST ast) {
241 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
242 currentFrame = currentFrame.pop();
243 }
244 }
245
246
247
248
249
250
251
252 private boolean isUnusedImport(String imprt) {
253 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
254 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
255 || javaLangPackageMatcher.matches();
256 }
257
258
259
260
261
262
263 private void processIdent(DetailAST ast) {
264 final DetailAST parent = ast.getParent();
265 final int parentType = parent.getType();
266
267 final boolean isClassOrMethod = parentType == TokenTypes.DOT
268 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
269
270 if (TokenUtil.isTypeDeclaration(parentType)) {
271 currentFrame.addDeclaredType(ast.getText());
272 }
273 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
274 currentFrame.addReferencedType(ast.getText());
275 }
276 }
277
278
279
280
281
282
283
284 private static boolean isQualifiedIdentifier(DetailAST ast) {
285 final DetailAST parent = ast.getParent();
286 final int parentType = parent.getType();
287
288 final boolean isQualifiedIdent = parentType == TokenTypes.DOT
289 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
290 && ast.getNextSibling() != null;
291 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
292 && ast.getNextSibling() != null;
293 return isQualifiedIdent || isQualifiedIdentFromMethodRef;
294 }
295
296
297
298
299
300
301 private void processImport(DetailAST ast) {
302 final FullIdent name = FullIdent.createFullIdentBelow(ast);
303 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
304 imports.add(name);
305 }
306 }
307
308
309
310
311
312
313 private void processStaticImport(DetailAST ast) {
314 final FullIdent name =
315 FullIdent.createFullIdent(
316 ast.getFirstChild().getNextSibling());
317 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
318 imports.add(name);
319 }
320 }
321
322
323
324
325
326
327
328 @SuppressWarnings("deprecation")
329 private void collectReferencesFromJavadoc(DetailAST ast) {
330 final FileContents contents = getFileContents();
331 final int lineNo = ast.getLineNo();
332 final TextBlock textBlock = contents.getJavadocBefore(lineNo);
333 if (textBlock != null) {
334 currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
335 }
336 }
337
338
339
340
341
342
343
344
345 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
346 final List<JavadocTag> tags = new ArrayList<>();
347
348
349 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE));
350
351 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK));
352
353 final Set<String> references = new HashSet<>();
354
355 tags.stream()
356 .filter(JavadocTag::canReferenceImports)
357 .forEach(tag -> references.addAll(processJavadocTag(tag)));
358 return references;
359 }
360
361
362
363
364
365
366
367
368 private static List<JavadocTag> getValidTags(TextBlock cmt,
369 JavadocUtil.JavadocTagType tagType) {
370 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags();
371 }
372
373
374
375
376
377
378
379 private static Set<String> processJavadocTag(JavadocTag tag) {
380 final Set<String> references = new HashSet<>();
381 final String identifier = tag.getFirstArg();
382 for (Pattern pattern : new Pattern[]
383 {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
384 references.addAll(matchPattern(identifier, pattern));
385 }
386 return references;
387 }
388
389
390
391
392
393
394
395
396
397 private static Set<String> matchPattern(String identifier, Pattern pattern) {
398 final Set<String> references = new HashSet<>();
399 final Matcher matcher = pattern.matcher(identifier);
400 while (matcher.find()) {
401 references.add(topLevelType(matcher.group(1)));
402 }
403 return references;
404 }
405
406
407
408
409
410
411
412
413
414 private static String topLevelType(String type) {
415 final String topLevelType;
416 final int dotIndex = type.indexOf('.');
417 if (dotIndex == -1) {
418 topLevelType = type;
419 }
420 else {
421 topLevelType = type.substring(0, dotIndex);
422 }
423 return topLevelType;
424 }
425
426
427
428
429 private static final class Frame {
430
431
432 private final Frame parent;
433
434
435 private final Set<String> declaredTypes;
436
437
438 private final Set<String> referencedTypes;
439
440
441
442
443
444
445 private Frame(Frame parent) {
446 this.parent = parent;
447 declaredTypes = new HashSet<>();
448 referencedTypes = new HashSet<>();
449 }
450
451
452
453
454
455
456 public void addDeclaredType(String type) {
457 declaredTypes.add(type);
458 }
459
460
461
462
463
464
465 public void addReferencedType(String type) {
466 referencedTypes.add(type);
467 }
468
469
470
471
472
473
474 public void addReferencedTypes(Collection<String> types) {
475 referencedTypes.addAll(types);
476 }
477
478
479
480
481
482 public void finish() {
483 referencedTypes.removeAll(declaredTypes);
484 }
485
486
487
488
489
490
491 public Frame push() {
492 return new Frame(this);
493 }
494
495
496
497
498
499
500 public Frame pop() {
501 finish();
502 parent.addReferencedTypes(referencedTypes);
503 return parent;
504 }
505
506
507
508
509
510
511
512 public boolean isReferencedType(String type) {
513 return referencedTypes.contains(type);
514 }
515
516
517
518
519
520
521 public static Frame compilationUnit() {
522 return new Frame(null);
523 }
524
525 }
526
527 }