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 }