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   * <li>
91   * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
92   * determine which packages or types are brought into scope by an {@code import module} declaration.
93   * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
94   * </li>
95   * </ul>
96   *
97   * @since 3.0
98   */
99  @FileStatefulCheck
100 public class UnusedImportsCheck extends AbstractCheck {
101 
102     /**
103      * A key is pointing to the warning message text in "messages.properties"
104      * file.
105      */
106     public static final String MSG_KEY = "import.unused";
107 
108     /** Regex to match class names. */
109     private static final Pattern CLASS_NAME = CommonUtil.createPattern(
110            "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
111     /** Regex to match the first class name. */
112     private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
113            "^" + CLASS_NAME);
114     /** Regex to match argument names. */
115     private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
116            "[(,]\\s*" + CLASS_NAME.pattern());
117 
118     /** Regexp pattern to match java.lang package. */
119     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
120         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
121 
122     /** Reference pattern. */
123     private static final Pattern REFERENCE = Pattern.compile(
124             "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$",
125             Pattern.CASE_INSENSITIVE
126     );
127 
128     /** Method pattern. */
129     private static final Pattern METHOD = Pattern.compile(
130             "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$",
131             Pattern.CASE_INSENSITIVE
132     );
133 
134     /** Suffix for the star import. */
135     private static final String STAR_IMPORT_SUFFIX = ".*";
136 
137     /** Set of the imports. */
138     private final Set<FullIdent> imports = new HashSet<>();
139 
140     /** Flag to indicate when time to start collecting references. */
141     private boolean collect;
142     /** Control whether to process Javadoc comments. */
143     private boolean processJavadoc = true;
144 
145     /**
146      * The scope is being processed.
147      * Types declared in a scope can shadow imported types.
148      */
149     private Frame currentFrame;
150 
151     /**
152      * Setter to control whether to process Javadoc comments.
153      *
154      * @param value Flag for processing Javadoc comments.
155      * @since 5.4
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         // loop over all the imports to see if referenced.
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             // Definitions that may contain Javadoc...
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             // Tokens for creating a new frame
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             }
220             case TokenTypes.IMPORT -> processImport(ast);
221             case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
222             case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
223             default -> {
224                 collect = true;
225                 if (processJavadoc) {
226                     collectReferencesFromJavadoc(ast);
227                 }
228             }
229         }
230     }
231 
232     @Override
233     public void leaveToken(DetailAST ast) {
234         if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
235             currentFrame = currentFrame.pop();
236         }
237     }
238 
239     /**
240      * Checks whether an import is unused.
241      *
242      * @param imprt an import.
243      * @return true if an import is unused.
244      */
245     private boolean isUnusedImport(String imprt) {
246         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
247         return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
248             || javaLangPackageMatcher.matches();
249     }
250 
251     /**
252      * Collects references made by IDENT.
253      *
254      * @param ast the IDENT node to process
255      */
256     private void processIdent(DetailAST ast) {
257         final DetailAST parent = ast.getParent();
258         final int parentType = parent.getType();
259 
260         final boolean isClassOrMethod = parentType == TokenTypes.DOT
261                 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
262 
263         if (TokenUtil.isTypeDeclaration(parentType)) {
264             currentFrame.addDeclaredType(ast.getText());
265         }
266         else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
267             currentFrame.addReferencedType(ast.getText());
268         }
269     }
270 
271     /**
272      * Checks whether ast is a fully qualified identifier.
273      *
274      * @param ast to check
275      * @return true if given ast is a fully qualified identifier
276      */
277     private static boolean isQualifiedIdentifier(DetailAST ast) {
278         final DetailAST parent = ast.getParent();
279         final int parentType = parent.getType();
280 
281         final boolean isQualifiedIdent = parentType == TokenTypes.DOT
282                 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
283                 && ast.getNextSibling() != null;
284         final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
285                 && ast.getNextSibling() != null;
286         return isQualifiedIdent || isQualifiedIdentFromMethodRef;
287     }
288 
289     /**
290      * Collects the details of imports.
291      *
292      * @param ast node containing the import details
293      */
294     private void processImport(DetailAST ast) {
295         final FullIdent name = FullIdent.createFullIdentBelow(ast);
296         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
297             imports.add(name);
298         }
299     }
300 
301     /**
302      * Collects the details of static imports.
303      *
304      * @param ast node containing the static import details
305      */
306     private void processStaticImport(DetailAST ast) {
307         final FullIdent name =
308             FullIdent.createFullIdent(
309                 ast.getFirstChild().getNextSibling());
310         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
311             imports.add(name);
312         }
313     }
314 
315     /**
316      * Collects references made in Javadoc comments.
317      *
318      * @param ast node to inspect for Javadoc
319      */
320     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
321     @SuppressWarnings("deprecation")
322     private void collectReferencesFromJavadoc(DetailAST ast) {
323         final FileContents contents = getFileContents();
324         final int lineNo = ast.getLineNo();
325         final TextBlock textBlock = contents.getJavadocBefore(lineNo);
326         if (textBlock != null) {
327             currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
328         }
329     }
330 
331     /**
332      * Process a javadoc {@link TextBlock} and return the set of classes
333      * referenced within.
334      *
335      * @param textBlock The javadoc block to parse
336      * @return a set of classes referenced in the javadoc block
337      */
338     private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
339         // Process INLINE tags
340         final List<JavadocTag> inlineTags = getTargetTags(textBlock,
341                 JavadocUtil.JavadocTagType.INLINE);
342         // Process BLOCK tags
343         final List<JavadocTag> blockTags = getTargetTags(textBlock,
344                 JavadocUtil.JavadocTagType.BLOCK);
345         final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream())
346                 .toList();
347 
348         final Set<String> references = new HashSet<>();
349 
350         targetTags.stream()
351             .filter(JavadocTag::canReferenceImports)
352             .forEach(tag -> references.addAll(processJavadocTag(tag)));
353         return references;
354     }
355 
356     /**
357      * Returns the list of valid tags found in a javadoc {@link TextBlock}.
358      * Filters tags based on whether they are inline or block tags, ensuring they match
359      * the correct format supported.
360      *
361      * @param cmt The javadoc block to parse
362      * @param javadocTagType The type of tags we're interested in
363      * @return the list of tags
364      */
365     private static List<JavadocTag> getTargetTags(TextBlock cmt,
366             JavadocUtil.JavadocTagType javadocTagType) {
367         return JavadocUtil.getJavadocTags(cmt, javadocTagType)
368             .getValidTags()
369             .stream()
370             .filter(tag -> isMatchingTagType(tag, javadocTagType))
371             .map(UnusedImportsCheck::bestTryToMatchReference)
372             .flatMap(Optional::stream)
373             .toList();
374     }
375 
376     /**
377      * Returns a list of references that found in a javadoc {@link JavadocTag}.
378      *
379      * @param tag The javadoc tag to parse
380      * @return A list of references that found in this tag
381      */
382     private static Set<String> processJavadocTag(JavadocTag tag) {
383         final Set<String> references = new HashSet<>();
384         final String identifier = tag.getFirstArg();
385         for (Pattern pattern : new Pattern[]
386         {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
387             references.addAll(matchPattern(identifier, pattern));
388         }
389         return references;
390     }
391 
392     /**
393      * Extracts a set of texts matching a {@link Pattern} from a
394      * {@link String}.
395      *
396      * @param identifier The String to match the pattern against
397      * @param pattern The Pattern used to extract the texts
398      * @return A set of texts which matched the pattern
399      */
400     private static Set<String> matchPattern(String identifier, Pattern pattern) {
401         final Set<String> references = new HashSet<>();
402         final Matcher matcher = pattern.matcher(identifier);
403         while (matcher.find()) {
404             references.add(topLevelType(matcher.group(1)));
405         }
406         return references;
407     }
408 
409     /**
410      * If the given type string contains "." (e.g. "Map.Entry"), returns the
411      * top level type (e.g. "Map"), as that is what must be imported for the
412      * type to resolve. Otherwise, returns the type as-is.
413      *
414      * @param type A possibly qualified type name
415      * @return The simple name of the top level type
416      */
417     private static String topLevelType(String type) {
418         final String topLevelType;
419         final int dotIndex = type.indexOf('.');
420         if (dotIndex == -1) {
421             topLevelType = type;
422         }
423         else {
424             topLevelType = type.substring(0, dotIndex);
425         }
426         return topLevelType;
427     }
428 
429     /**
430      * Checks if a Javadoc tag matches the expected type based on its extraction format.
431      * This method checks if an inline tag is extracted as a block tag or vice versa.
432      * It ensures that block tags are correctly recognized as block tags and inline tags
433      * as inline tags during processing.
434      *
435      * @param tag The Javadoc tag to check.
436      * @param javadocTagType The expected type of the tag (BLOCK or INLINE).
437      * @return {@code true} if the tag matches the expected type, otherwise {@code false}.
438      */
439     private static boolean isMatchingTagType(JavadocTag tag,
440                                              JavadocUtil.JavadocTagType javadocTagType) {
441         final boolean isInlineTag = tag.isInlineTag();
442         final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK;
443 
444         return isBlockTagType != isInlineTag;
445     }
446 
447     /**
448      * Attempts to match a reference string against a predefined pattern
449      * and extracts valid reference.
450      *
451      * @param tag the input tag to check
452      * @return Optional of extracted references
453      */
454     public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) {
455         final String content = tag.getFirstArg();
456         final int referenceIndex = extractReferencePart(content);
457         Optional<JavadocTag> validTag = Optional.empty();
458 
459         if (referenceIndex != -1) {
460             final String referenceString;
461             if (referenceIndex == 0) {
462                 referenceString = content;
463             }
464             else {
465                 referenceString = content.substring(0, referenceIndex);
466             }
467             final Matcher matcher = REFERENCE.matcher(referenceString);
468             if (matcher.matches()) {
469                 final int methodIndex = 3;
470                 final String methodPart = matcher.group(methodIndex);
471                 final boolean isValid = methodPart == null
472                         || METHOD.matcher(methodPart).matches();
473                 if (isValid) {
474                     validTag = Optional.of(tag);
475                 }
476             }
477         }
478         return validTag;
479     }
480 
481     /**
482      * Extracts the reference part from an input string while ensuring balanced parentheses.
483      *
484      * @param input the input string
485      * @return -1 if parentheses are unbalanced, 0 if no method is found,
486      *         or the index of the first space outside parentheses.
487      */
488     private static @IndexOrLow("#1")int extractReferencePart(String input) {
489         int parenthesesCount = 0;
490         int firstSpaceOutsideParens = -1;
491         for (int index = 0; index < input.length(); index++) {
492             final char currentCharacter = input.charAt(index);
493 
494             if (currentCharacter == '(') {
495                 parenthesesCount++;
496             }
497             else if (currentCharacter == ')') {
498                 parenthesesCount--;
499             }
500             else if (currentCharacter == ' ' && parenthesesCount == 0) {
501                 firstSpaceOutsideParens = index;
502                 break;
503             }
504         }
505 
506         int methodIndex = -1;
507         if (parenthesesCount == 0) {
508             if (firstSpaceOutsideParens == -1) {
509                 methodIndex = 0;
510             }
511             else {
512                 methodIndex = firstSpaceOutsideParens;
513             }
514         }
515         return methodIndex;
516     }
517 
518     /**
519      * Holds the names of referenced types and names of declared inner types.
520      */
521     private static final class Frame {
522 
523         /** Parent frame. */
524         private final Frame parent;
525 
526         /** Nested types declared in the current scope. */
527         private final Set<String> declaredTypes;
528 
529         /** Set of references - possibly to imports or locally declared types. */
530         private final Set<String> referencedTypes;
531 
532         /**
533          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
534          *
535          * @param parent the parent frame
536          */
537         private Frame(Frame parent) {
538             this.parent = parent;
539             declaredTypes = new HashSet<>();
540             referencedTypes = new HashSet<>();
541         }
542 
543         /**
544          * Adds new inner type.
545          *
546          * @param type the type name
547          */
548         public void addDeclaredType(String type) {
549             declaredTypes.add(type);
550         }
551 
552         /**
553          * Adds new type reference to the current frame.
554          *
555          * @param type the type name
556          */
557         public void addReferencedType(String type) {
558             referencedTypes.add(type);
559         }
560 
561         /**
562          * Adds new inner types.
563          *
564          * @param types the type names
565          */
566         public void addReferencedTypes(Collection<String> types) {
567             referencedTypes.addAll(types);
568         }
569 
570         /**
571          * Filters out all references to locally defined types.
572          *
573          */
574         public void finish() {
575             referencedTypes.removeAll(declaredTypes);
576         }
577 
578         /**
579          * Creates new inner frame.
580          *
581          * @return a new frame.
582          */
583         public Frame push() {
584             return new Frame(this);
585         }
586 
587         /**
588          * Pulls all referenced types up, except those that are declared in this scope.
589          *
590          * @return the parent frame
591          */
592         public Frame pop() {
593             finish();
594             parent.addReferencedTypes(referencedTypes);
595             return parent;
596         }
597 
598         /**
599          * Checks whether this type name is used in this frame.
600          *
601          * @param type the type name
602          * @return {@code true} if the type is used
603          */
604         public boolean isReferencedType(String type) {
605             return referencedTypes.contains(type);
606         }
607 
608         /**
609          * Creates a new top-level frame for the compilation unit.
610          *
611          * @return a new frame.
612          */
613         public static Frame compilationUnit() {
614             return new Frame(null);
615         }
616 
617     }
618 
619 }