View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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   * <div>
47   * Checks for unused import statements. An import statement
48   * is considered unused if:
49   * </div>
50   *
51   * <ul>
52   * <li>
53   * It is not referenced in the file. The algorithm does not support wild-card
54   * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
55   * checks for imports that handle wild-card imports.
56   * </li>
57   * <li>
58   * The class imported is from the {@code java.lang} package. For example
59   * importing {@code java.lang.String}.
60   * </li>
61   * <li>
62   * The class imported is from the same package.
63   * </li>
64   * <li>
65   * A static method is imported when used as method reference. In that case,
66   * only the type needs to be imported and that's enough to resolve the method.
67   * </li>
68   * <li>
69   * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
70   * default, but it is considered bad practice to introduce a compile-time
71   * dependency for documentation purposes only. As an example, the import
72   * {@code java.util.List} would be considered referenced with the Javadoc
73   * comment {@code {@link List}}. The alternative to avoid introducing a compile-time
74   * dependency would be to write the Javadoc comment as {@code {&#64;link java.util.List}}.
75   * </li>
76   * </ul>
77   *
78   * <p>
79   * The main limitation of this check is handling the cases where:
80   * </p>
81   * <ul>
82   * <li>
83   * An imported type has the same name as a declaration, such as a member variable.
84   * </li>
85   * <li>
86   * There are two or more static imports with the same method name
87   * (javac can distinguish imports with same name but different parameters, but checkstyle can not
88   * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
89   * </li>
90   * </ul>
91   * <ul>
92   * <li>
93   * Property {@code processJavadoc} - Control whether to process Javadoc comments.
94   * Type is {@code boolean}.
95   * Default value is {@code true}.
96   * </li>
97   * </ul>
98   *
99   * <p>
100  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
101  * </p>
102  *
103  * <p>
104  * Violation Message Keys:
105  * </p>
106  * <ul>
107  * <li>
108  * {@code import.unused}
109  * </li>
110  * </ul>
111  *
112  * @since 3.0
113  */
114 @FileStatefulCheck
115 public class UnusedImportsCheck extends AbstractCheck {
116 
117     /**
118      * A key is pointing to the warning message text in "messages.properties"
119      * file.
120      */
121     public static final String MSG_KEY = "import.unused";
122 
123     /** Regex to match class names. */
124     private static final Pattern CLASS_NAME = CommonUtil.createPattern(
125            "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
126     /** Regex to match the first class name. */
127     private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
128            "^" + CLASS_NAME);
129     /** Regex to match argument names. */
130     private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
131            "[(,]\\s*" + CLASS_NAME.pattern());
132 
133     /** Regexp pattern to match java.lang package. */
134     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
135         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
136 
137     /** Reference pattern. */
138     private static final Pattern REFERENCE = Pattern.compile(
139             "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$",
140             Pattern.CASE_INSENSITIVE
141     );
142 
143     /** Method pattern. */
144     private static final Pattern METHOD = Pattern.compile(
145             "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$",
146             Pattern.CASE_INSENSITIVE
147     );
148 
149     /** Suffix for the star import. */
150     private static final String STAR_IMPORT_SUFFIX = ".*";
151 
152     /** Set of the imports. */
153     private final Set<FullIdent> imports = new HashSet<>();
154 
155     /** Flag to indicate when time to start collecting references. */
156     private boolean collect;
157     /** Control whether to process Javadoc comments. */
158     private boolean processJavadoc = true;
159 
160     /**
161      * The scope is being processed.
162      * Types declared in a scope can shadow imported types.
163      */
164     private Frame currentFrame;
165 
166     /**
167      * Setter to control whether to process Javadoc comments.
168      *
169      * @param value Flag for processing Javadoc comments.
170      * @since 5.4
171      */
172     public void setProcessJavadoc(boolean value) {
173         processJavadoc = value;
174     }
175 
176     @Override
177     public void beginTree(DetailAST rootAST) {
178         collect = false;
179         currentFrame = Frame.compilationUnit();
180         imports.clear();
181     }
182 
183     @Override
184     public void finishTree(DetailAST rootAST) {
185         currentFrame.finish();
186         // loop over all the imports to see if referenced.
187         imports.stream()
188             .filter(imprt -> isUnusedImport(imprt.getText()))
189             .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
190     }
191 
192     @Override
193     public int[] getDefaultTokens() {
194         return getRequiredTokens();
195     }
196 
197     @Override
198     public int[] getRequiredTokens() {
199         return new int[] {
200             TokenTypes.IDENT,
201             TokenTypes.IMPORT,
202             TokenTypes.STATIC_IMPORT,
203             // Definitions that may contain Javadoc...
204             TokenTypes.PACKAGE_DEF,
205             TokenTypes.ANNOTATION_DEF,
206             TokenTypes.ANNOTATION_FIELD_DEF,
207             TokenTypes.ENUM_DEF,
208             TokenTypes.ENUM_CONSTANT_DEF,
209             TokenTypes.CLASS_DEF,
210             TokenTypes.INTERFACE_DEF,
211             TokenTypes.METHOD_DEF,
212             TokenTypes.CTOR_DEF,
213             TokenTypes.VARIABLE_DEF,
214             TokenTypes.RECORD_DEF,
215             TokenTypes.COMPACT_CTOR_DEF,
216             // Tokens for creating a new frame
217             TokenTypes.OBJBLOCK,
218             TokenTypes.SLIST,
219         };
220     }
221 
222     @Override
223     public int[] getAcceptableTokens() {
224         return getRequiredTokens();
225     }
226 
227     @Override
228     public void visitToken(DetailAST ast) {
229         switch (ast.getType()) {
230             case TokenTypes.IDENT -> {
231                 if (collect) {
232                     processIdent(ast);
233                 }
234             }
235             case TokenTypes.IMPORT -> processImport(ast);
236             case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
237             case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
238             default -> {
239                 collect = true;
240                 if (processJavadoc) {
241                     collectReferencesFromJavadoc(ast);
242                 }
243             }
244         }
245     }
246 
247     @Override
248     public void leaveToken(DetailAST ast) {
249         if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
250             currentFrame = currentFrame.pop();
251         }
252     }
253 
254     /**
255      * Checks whether an import is unused.
256      *
257      * @param imprt an import.
258      * @return true if an import is unused.
259      */
260     private boolean isUnusedImport(String imprt) {
261         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
262         return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
263             || javaLangPackageMatcher.matches();
264     }
265 
266     /**
267      * Collects references made by IDENT.
268      *
269      * @param ast the IDENT node to process
270      */
271     private void processIdent(DetailAST ast) {
272         final DetailAST parent = ast.getParent();
273         final int parentType = parent.getType();
274 
275         final boolean isClassOrMethod = parentType == TokenTypes.DOT
276                 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
277 
278         if (TokenUtil.isTypeDeclaration(parentType)) {
279             currentFrame.addDeclaredType(ast.getText());
280         }
281         else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
282             currentFrame.addReferencedType(ast.getText());
283         }
284     }
285 
286     /**
287      * Checks whether ast is a fully qualified identifier.
288      *
289      * @param ast to check
290      * @return true if given ast is a fully qualified identifier
291      */
292     private static boolean isQualifiedIdentifier(DetailAST ast) {
293         final DetailAST parent = ast.getParent();
294         final int parentType = parent.getType();
295 
296         final boolean isQualifiedIdent = parentType == TokenTypes.DOT
297                 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
298                 && ast.getNextSibling() != null;
299         final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
300                 && ast.getNextSibling() != null;
301         return isQualifiedIdent || isQualifiedIdentFromMethodRef;
302     }
303 
304     /**
305      * Collects the details of imports.
306      *
307      * @param ast node containing the import details
308      */
309     private void processImport(DetailAST ast) {
310         final FullIdent name = FullIdent.createFullIdentBelow(ast);
311         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
312             imports.add(name);
313         }
314     }
315 
316     /**
317      * Collects the details of static imports.
318      *
319      * @param ast node containing the static import details
320      */
321     private void processStaticImport(DetailAST ast) {
322         final FullIdent name =
323             FullIdent.createFullIdent(
324                 ast.getFirstChild().getNextSibling());
325         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
326             imports.add(name);
327         }
328     }
329 
330     /**
331      * Collects references made in Javadoc comments.
332      *
333      * @param ast node to inspect for Javadoc
334      */
335     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
336     @SuppressWarnings("deprecation")
337     private void collectReferencesFromJavadoc(DetailAST ast) {
338         final FileContents contents = getFileContents();
339         final int lineNo = ast.getLineNo();
340         final TextBlock textBlock = contents.getJavadocBefore(lineNo);
341         if (textBlock != null) {
342             currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
343         }
344     }
345 
346     /**
347      * Process a javadoc {@link TextBlock} and return the set of classes
348      * referenced within.
349      *
350      * @param textBlock The javadoc block to parse
351      * @return a set of classes referenced in the javadoc block
352      */
353     private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
354         // Process INLINE tags
355         final List<JavadocTag> inlineTags = getTargetTags(textBlock,
356                 JavadocUtil.JavadocTagType.INLINE);
357         // Process BLOCK tags
358         final List<JavadocTag> blockTags = getTargetTags(textBlock,
359                 JavadocUtil.JavadocTagType.BLOCK);
360         final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream())
361                 .toList();
362 
363         final Set<String> references = new HashSet<>();
364 
365         targetTags.stream()
366             .filter(JavadocTag::canReferenceImports)
367             .forEach(tag -> references.addAll(processJavadocTag(tag)));
368         return references;
369     }
370 
371     /**
372      * Returns the list of valid tags found in a javadoc {@link TextBlock}.
373      * Filters tags based on whether they are inline or block tags, ensuring they match
374      * the correct format supported.
375      *
376      * @param cmt The javadoc block to parse
377      * @param javadocTagType The type of tags we're interested in
378      * @return the list of tags
379      */
380     private static List<JavadocTag> getTargetTags(TextBlock cmt,
381             JavadocUtil.JavadocTagType javadocTagType) {
382         return JavadocUtil.getJavadocTags(cmt, javadocTagType)
383             .getValidTags()
384             .stream()
385             .filter(tag -> isMatchingTagType(tag, javadocTagType))
386             .map(UnusedImportsCheck::bestTryToMatchReference)
387             .flatMap(Optional::stream)
388             .toList();
389     }
390 
391     /**
392      * Returns a list of references that found in a javadoc {@link JavadocTag}.
393      *
394      * @param tag The javadoc tag to parse
395      * @return A list of references that found in this tag
396      */
397     private static Set<String> processJavadocTag(JavadocTag tag) {
398         final Set<String> references = new HashSet<>();
399         final String identifier = tag.getFirstArg();
400         for (Pattern pattern : new Pattern[]
401         {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
402             references.addAll(matchPattern(identifier, pattern));
403         }
404         return references;
405     }
406 
407     /**
408      * Extracts a set of texts matching a {@link Pattern} from a
409      * {@link String}.
410      *
411      * @param identifier The String to match the pattern against
412      * @param pattern The Pattern used to extract the texts
413      * @return A set of texts which matched the pattern
414      */
415     private static Set<String> matchPattern(String identifier, Pattern pattern) {
416         final Set<String> references = new HashSet<>();
417         final Matcher matcher = pattern.matcher(identifier);
418         while (matcher.find()) {
419             references.add(topLevelType(matcher.group(1)));
420         }
421         return references;
422     }
423 
424     /**
425      * If the given type string contains "." (e.g. "Map.Entry"), returns the
426      * top level type (e.g. "Map"), as that is what must be imported for the
427      * type to resolve. Otherwise, returns the type as-is.
428      *
429      * @param type A possibly qualified type name
430      * @return The simple name of the top level type
431      */
432     private static String topLevelType(String type) {
433         final String topLevelType;
434         final int dotIndex = type.indexOf('.');
435         if (dotIndex == -1) {
436             topLevelType = type;
437         }
438         else {
439             topLevelType = type.substring(0, dotIndex);
440         }
441         return topLevelType;
442     }
443 
444     /**
445      * Checks if a Javadoc tag matches the expected type based on its extraction format.
446      * This method checks if an inline tag is extracted as a block tag or vice versa.
447      * It ensures that block tags are correctly recognized as block tags and inline tags
448      * as inline tags during processing.
449      *
450      * @param tag The Javadoc tag to check.
451      * @param javadocTagType The expected type of the tag (BLOCK or INLINE).
452      * @return {@code true} if the tag matches the expected type, otherwise {@code false}.
453      */
454     private static boolean isMatchingTagType(JavadocTag tag,
455                                              JavadocUtil.JavadocTagType javadocTagType) {
456         final boolean isInlineTag = tag.isInlineTag();
457         final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK;
458 
459         return isBlockTagType != isInlineTag;
460     }
461 
462     /**
463      * Attempts to match a reference string against a predefined pattern
464      * and extracts valid reference.
465      *
466      * @param tag the input tag to check
467      * @return Optional of extracted references
468      */
469     public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) {
470         final String content = tag.getFirstArg();
471         final int referenceIndex = extractReferencePart(content);
472         Optional<JavadocTag> validTag = Optional.empty();
473 
474         if (referenceIndex != -1) {
475             final String referenceString;
476             if (referenceIndex == 0) {
477                 referenceString = content;
478             }
479             else {
480                 referenceString = content.substring(0, referenceIndex);
481             }
482             final Matcher matcher = REFERENCE.matcher(referenceString);
483             if (matcher.matches()) {
484                 final int methodIndex = 3;
485                 final String methodPart = matcher.group(methodIndex);
486                 final boolean isValid = methodPart == null
487                         || METHOD.matcher(methodPart).matches();
488                 if (isValid) {
489                     validTag = Optional.of(tag);
490                 }
491             }
492         }
493         return validTag;
494     }
495 
496     /**
497      * Extracts the reference part from an input string while ensuring balanced parentheses.
498      *
499      * @param input the input string
500      * @return -1 if parentheses are unbalanced, 0 if no method is found,
501      *         or the index of the first space outside parentheses.
502      */
503     private static @IndexOrLow("#1")int extractReferencePart(String input) {
504         int parenthesesCount = 0;
505         int firstSpaceOutsideParens = -1;
506         for (int index = 0; index < input.length(); index++) {
507             final char currentCharacter = input.charAt(index);
508 
509             if (currentCharacter == '(') {
510                 parenthesesCount++;
511             }
512             else if (currentCharacter == ')') {
513                 parenthesesCount--;
514             }
515             else if (currentCharacter == ' ' && parenthesesCount == 0) {
516                 firstSpaceOutsideParens = index;
517                 break;
518             }
519         }
520 
521         int methodIndex = -1;
522         if (parenthesesCount == 0) {
523             if (firstSpaceOutsideParens == -1) {
524                 methodIndex = 0;
525             }
526             else {
527                 methodIndex = firstSpaceOutsideParens;
528             }
529         }
530         return methodIndex;
531     }
532 
533     /**
534      * Holds the names of referenced types and names of declared inner types.
535      */
536     private static final class Frame {
537 
538         /** Parent frame. */
539         private final Frame parent;
540 
541         /** Nested types declared in the current scope. */
542         private final Set<String> declaredTypes;
543 
544         /** Set of references - possibly to imports or locally declared types. */
545         private final Set<String> referencedTypes;
546 
547         /**
548          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
549          *
550          * @param parent the parent frame
551          */
552         private Frame(Frame parent) {
553             this.parent = parent;
554             declaredTypes = new HashSet<>();
555             referencedTypes = new HashSet<>();
556         }
557 
558         /**
559          * Adds new inner type.
560          *
561          * @param type the type name
562          */
563         public void addDeclaredType(String type) {
564             declaredTypes.add(type);
565         }
566 
567         /**
568          * Adds new type reference to the current frame.
569          *
570          * @param type the type name
571          */
572         public void addReferencedType(String type) {
573             referencedTypes.add(type);
574         }
575 
576         /**
577          * Adds new inner types.
578          *
579          * @param types the type names
580          */
581         public void addReferencedTypes(Collection<String> types) {
582             referencedTypes.addAll(types);
583         }
584 
585         /**
586          * Filters out all references to locally defined types.
587          *
588          */
589         public void finish() {
590             referencedTypes.removeAll(declaredTypes);
591         }
592 
593         /**
594          * Creates new inner frame.
595          *
596          * @return a new frame.
597          */
598         public Frame push() {
599             return new Frame(this);
600         }
601 
602         /**
603          * Pulls all referenced types up, except those that are declared in this scope.
604          *
605          * @return the parent frame
606          */
607         public Frame pop() {
608             finish();
609             parent.addReferencedTypes(referencedTypes);
610             return parent;
611         }
612 
613         /**
614          * Checks whether this type name is used in this frame.
615          *
616          * @param type the type name
617          * @return {@code true} if the type is used
618          */
619         public boolean isReferencedType(String type) {
620             return referencedTypes.contains(type);
621         }
622 
623         /**
624          * Creates a new top-level frame for the compilation unit.
625          *
626          * @return a new frame.
627          */
628         public static Frame compilationUnit() {
629             return new Frame(null);
630         }
631 
632     }
633 
634 }