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