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   *
92   * @since 3.0
93   */
94  @FileStatefulCheck
95  public class UnusedImportsCheck extends AbstractCheck {
96  
97      /**
98       * A key is pointing to the warning message text in "messages.properties"
99       * file.
100      */
101     public static final String MSG_KEY = "import.unused";
102 
103     /** Regex to match class names. */
104     private static final Pattern CLASS_NAME = CommonUtil.createPattern(
105            "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
106     /** Regex to match the first class name. */
107     private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
108            "^" + CLASS_NAME);
109     /** Regex to match argument names. */
110     private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
111            "[(,]\\s*" + CLASS_NAME.pattern());
112 
113     /** Regexp pattern to match java.lang package. */
114     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
115         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
116 
117     /** Reference pattern. */
118     private static final Pattern REFERENCE = Pattern.compile(
119             "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$",
120             Pattern.CASE_INSENSITIVE
121     );
122 
123     /** Method pattern. */
124     private static final Pattern METHOD = Pattern.compile(
125             "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$",
126             Pattern.CASE_INSENSITIVE
127     );
128 
129     /** Suffix for the star import. */
130     private static final String STAR_IMPORT_SUFFIX = ".*";
131 
132     /** Set of the imports. */
133     private final Set<FullIdent> imports = new HashSet<>();
134 
135     /** Flag to indicate when time to start collecting references. */
136     private boolean collect;
137     /** Control whether to process Javadoc comments. */
138     private boolean processJavadoc = true;
139 
140     /**
141      * The scope is being processed.
142      * Types declared in a scope can shadow imported types.
143      */
144     private Frame currentFrame;
145 
146     /**
147      * Setter to control whether to process Javadoc comments.
148      *
149      * @param value Flag for processing Javadoc comments.
150      * @since 5.4
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         // loop over all the imports to see if referenced.
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             // Definitions that may contain Javadoc...
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             // Tokens for creating a new frame
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      * Checks whether an import is unused.
236      *
237      * @param imprt an import.
238      * @return true if an import is unused.
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      * Collects references made by IDENT.
248      *
249      * @param ast the IDENT node to process
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      * Checks whether ast is a fully qualified identifier.
268      *
269      * @param ast to check
270      * @return true if given ast is a fully qualified identifier
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      * Collects the details of imports.
286      *
287      * @param ast node containing the import details
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      * Collects the details of static imports.
298      *
299      * @param ast node containing the static import details
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      * Collects references made in Javadoc comments.
312      *
313      * @param ast node to inspect for Javadoc
314      */
315     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
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      * Process a javadoc {@link TextBlock} and return the set of classes
328      * referenced within.
329      *
330      * @param textBlock The javadoc block to parse
331      * @return a set of classes referenced in the javadoc block
332      */
333     private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
334         // Process INLINE tags
335         final List<JavadocTag> inlineTags = getTargetTags(textBlock,
336                 JavadocUtil.JavadocTagType.INLINE);
337         // Process BLOCK tags
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      * Returns the list of valid tags found in a javadoc {@link TextBlock}.
353      * Filters tags based on whether they are inline or block tags, ensuring they match
354      * the correct format supported.
355      *
356      * @param cmt The javadoc block to parse
357      * @param javadocTagType The type of tags we're interested in
358      * @return the list of tags
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      * Returns a list of references that found in a javadoc {@link JavadocTag}.
373      *
374      * @param tag The javadoc tag to parse
375      * @return A list of references that found in this tag
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      * Extracts a set of texts matching a {@link Pattern} from a
389      * {@link String}.
390      *
391      * @param identifier The String to match the pattern against
392      * @param pattern The Pattern used to extract the texts
393      * @return A set of texts which matched the pattern
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      * If the given type string contains "." (e.g. "Map.Entry"), returns the
406      * top level type (e.g. "Map"), as that is what must be imported for the
407      * type to resolve. Otherwise, returns the type as-is.
408      *
409      * @param type A possibly qualified type name
410      * @return The simple name of the top level type
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      * Checks if a Javadoc tag matches the expected type based on its extraction format.
426      * This method checks if an inline tag is extracted as a block tag or vice versa.
427      * It ensures that block tags are correctly recognized as block tags and inline tags
428      * as inline tags during processing.
429      *
430      * @param tag The Javadoc tag to check.
431      * @param javadocTagType The expected type of the tag (BLOCK or INLINE).
432      * @return {@code true} if the tag matches the expected type, otherwise {@code false}.
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      * Attempts to match a reference string against a predefined pattern
444      * and extracts valid reference.
445      *
446      * @param tag the input tag to check
447      * @return Optional of extracted references
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      * Extracts the reference part from an input string while ensuring balanced parentheses.
478      *
479      * @param input the input string
480      * @return -1 if parentheses are unbalanced, 0 if no method is found,
481      *         or the index of the first space outside parentheses.
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      * Holds the names of referenced types and names of declared inner types.
515      */
516     private static final class Frame {
517 
518         /** Parent frame. */
519         private final Frame parent;
520 
521         /** Nested types declared in the current scope. */
522         private final Set<String> declaredTypes;
523 
524         /** Set of references - possibly to imports or locally declared types. */
525         private final Set<String> referencedTypes;
526 
527         /**
528          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
529          *
530          * @param parent the parent frame
531          */
532         private Frame(Frame parent) {
533             this.parent = parent;
534             declaredTypes = new HashSet<>();
535             referencedTypes = new HashSet<>();
536         }
537 
538         /**
539          * Adds new inner type.
540          *
541          * @param type the type name
542          */
543         public void addDeclaredType(String type) {
544             declaredTypes.add(type);
545         }
546 
547         /**
548          * Adds new type reference to the current frame.
549          *
550          * @param type the type name
551          */
552         public void addReferencedType(String type) {
553             referencedTypes.add(type);
554         }
555 
556         /**
557          * Adds new inner types.
558          *
559          * @param types the type names
560          */
561         public void addReferencedTypes(Collection<String> types) {
562             referencedTypes.addAll(types);
563         }
564 
565         /**
566          * Filters out all references to locally defined types.
567          *
568          */
569         public void finish() {
570             referencedTypes.removeAll(declaredTypes);
571         }
572 
573         /**
574          * Creates new inner frame.
575          *
576          * @return a new frame.
577          */
578         public Frame push() {
579             return new Frame(this);
580         }
581 
582         /**
583          * Pulls all referenced types up, except those that are declared in this scope.
584          *
585          * @return the parent frame
586          */
587         public Frame pop() {
588             finish();
589             parent.addReferencedTypes(referencedTypes);
590             return parent;
591         }
592 
593         /**
594          * Checks whether this type name is used in this frame.
595          *
596          * @param type the type name
597          * @return {@code true} if the type is used
598          */
599         public boolean isReferencedType(String type) {
600             return referencedTypes.contains(type);
601         }
602 
603         /**
604          * Creates a new top-level frame for the compilation unit.
605          *
606          * @return a new frame.
607          */
608         public static Frame compilationUnit() {
609             return new Frame(null);
610         }
611 
612     }
613 
614 }