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.ArrayList;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.DetailNode;
33  import com.puppycrawl.tools.checkstyle.api.FullIdent;
34  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
40  
41  /**
42   * <div>
43   * Checks for unused import statements. An import statement
44   * is considered unused if:
45   * </div>
46   *
47   * <ul>
48   * <li>
49   * It is not referenced in the file. The algorithm does not support wild-card
50   * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
51   * checks for imports that handle wild-card imports.
52   * </li>
53   * <li>
54   * The class imported is from the {@code java.lang} package. For example
55   * importing {@code java.lang.String}.
56   * </li>
57   * <li>
58   * The class imported is from the same package.
59   * </li>
60   * <li>
61   * A static method is imported when used as method reference. In that case,
62   * only the type needs to be imported and that's enough to resolve the method.
63   * </li>
64   * <li>
65   * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
66   * default, but it is considered bad practice to introduce a compile-time
67   * dependency for documentation purposes only. As an example, the import
68   * {@code java.util.Set} would be considered referenced with the Javadoc
69   * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time
70   * dependency would be to write the Javadoc comment as {@code {@link Set}}.
71   * </li>
72   * </ul>
73   *
74   * <p>
75   * The main limitation of this check is handling the cases where:
76   * </p>
77   * <ul>
78   * <li>
79   * An imported type has the same name as a declaration, such as a member variable.
80   * </li>
81   * <li>
82   * There are two or more static imports with the same method name
83   * (javac can distinguish imports with same name but different parameters, but checkstyle can not
84   * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
85   * </li>
86   * <li>
87   * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
88   * determine which packages or types are brought into scope by an {@code import module} declaration.
89   * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
90   * </li>
91   * </ul>
92   *
93   * @since 3.0
94   */
95  @FileStatefulCheck
96  @SuppressWarnings("UnrecognisedJavadocTag")
97  public class UnusedImportsCheck extends AbstractJavadocCheck {
98  
99      /**
100      * A key is pointing to the warning message text in "messages.properties"
101      * file.
102      */
103     public static final String MSG_KEY = "import.unused";
104 
105     /** Regexp pattern to match java.lang package. */
106     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
107         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
108 
109     /** Suffix for the star import. */
110     private static final String STAR_IMPORT_SUFFIX = ".*";
111 
112     /** Prefix for wildcard extends bound. */
113     private static final String WILDCARD_EXTENDS_PREFIX = "? extends ";
114 
115     /** Prefix for wildcard super bound. */
116     private static final String WILDCARD_SUPER_PREFIX = "? super ";
117 
118     /** Pattern for a valid Java identifier (parameter name). */
119     private static final Pattern PARAM_NAME_PATTERN =
120             Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*");
121 
122     /** Set of the imports. */
123     private final Set<FullIdent> imports = new HashSet<>();
124 
125     /** Control whether to process Javadoc comments. */
126     private boolean processJavadoc = true;
127 
128     /**
129      * The scope is being processed.
130      * Types declared in a scope can shadow imported types.
131      */
132     private Frame currentFrame;
133 
134     /**
135      * Creates a new {@code UnusedImportsCheck} instance.
136      */
137     public UnusedImportsCheck() {
138         // no code by default
139     }
140 
141     /**
142      * Setter to control whether to process Javadoc comments.
143      *
144      * @param value Flag for processing Javadoc comments.
145      * @since 5.4
146      */
147     public void setProcessJavadoc(boolean value) {
148         processJavadoc = value;
149     }
150 
151     /**
152      * Setter to control when to print violations if the Javadoc being examined by this check
153      * violates the tight html rules defined at
154      * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
155      *     Tight-HTML Rules</a>.
156      *
157      * @param shouldReportViolation value to which the field shall be set to
158      * @since 8.3
159      * @propertySince 13.4.0
160      */
161     @Override
162     public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
163         super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
164     }
165 
166     @Override
167     public void beginTree(DetailAST rootAST) {
168         super.beginTree(rootAST);
169         currentFrame = Frame.compilationUnit();
170         imports.clear();
171     }
172 
173     @Override
174     public void finishTree(DetailAST rootAST) {
175         currentFrame.finish();
176         // loop over all the imports to see if referenced.
177         imports.stream()
178             .filter(imprt -> isUnusedImport(imprt.getText()))
179             .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
180     }
181 
182     @Override
183     public int[] getRequiredJavadocTokens() {
184         return new int[] {
185             JavadocCommentsTokenTypes.REFERENCE,
186             JavadocCommentsTokenTypes.PARAMETER_TYPE,
187             JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
188             JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
189         };
190     }
191 
192     @Override
193     public int[] getDefaultJavadocTokens() {
194         return getRequiredJavadocTokens();
195     }
196 
197     @Override
198     public void visitJavadocToken(DetailNode ast) {
199         switch (ast.getType()) {
200             case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast);
201             case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast);
202             case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
203                  JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast);
204             default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
205         }
206 
207     }
208 
209     @Override
210     public int[] getDefaultTokens() {
211         return getRequiredTokens();
212     }
213 
214     @Override
215     public int[] getAcceptableTokens() {
216         return getRequiredTokens();
217     }
218 
219     @Override
220     public int[] getRequiredTokens() {
221         return new int[] {
222             TokenTypes.IDENT,
223             TokenTypes.IMPORT,
224             TokenTypes.STATIC_IMPORT,
225             // Tokens for creating a new frame
226             TokenTypes.OBJBLOCK,
227             TokenTypes.SLIST,
228             // Javadoc
229             TokenTypes.BLOCK_COMMENT_BEGIN,
230         };
231     }
232 
233     @Override
234     public void visitToken(DetailAST ast) {
235         switch (ast.getType()) {
236             case TokenTypes.IDENT -> processIdent(ast);
237             case TokenTypes.IMPORT -> processImport(ast);
238             case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
239             case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
240             case TokenTypes.BLOCK_COMMENT_BEGIN -> {
241                 if (processJavadoc) {
242                     super.visitToken(ast);
243                 }
244             }
245             default -> throw new IllegalArgumentException("Unknown token type " + ast);
246         }
247     }
248 
249     @Override
250     public void leaveToken(DetailAST ast) {
251         if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
252             currentFrame = currentFrame.pop();
253         }
254     }
255 
256     /**
257      * Checks whether an import is unused.
258      *
259      * @param imprt an import.
260      * @return true if an import is unused.
261      */
262     private boolean isUnusedImport(String imprt) {
263         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
264         return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
265             || javaLangPackageMatcher.matches();
266     }
267 
268     /**
269      * Collects references made by IDENT.
270      *
271      * @param ast the IDENT node to process
272      */
273     private void processIdent(DetailAST ast) {
274         final DetailAST parent = ast.getParent();
275         final int parentType = parent.getType();
276 
277         // Ignore IDENTs that are part of the import statement itself
278         final boolean collect = parentType != TokenTypes.IMPORT
279                 && parentType != TokenTypes.STATIC_IMPORT;
280 
281         if (collect) {
282             final boolean isClassOrMethod = parentType == TokenTypes.DOT
283                 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
284 
285             if (TokenUtil.isTypeDeclaration(parentType)) {
286                 currentFrame.addDeclaredType(ast.getText());
287             }
288             else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
289                 currentFrame.addReferencedType(ast.getText());
290             }
291         }
292     }
293 
294     /**
295      * Checks whether ast is a fully qualified identifier.
296      *
297      * @param ast to check
298      * @return true if given ast is a fully qualified identifier
299      */
300     private static boolean isQualifiedIdentifier(DetailAST ast) {
301         final DetailAST parent = ast.getParent();
302         final int parentType = parent.getType();
303 
304         final boolean isQualifiedIdent = parentType == TokenTypes.DOT
305                 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
306                 && ast.getNextSibling() != null;
307         final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
308                 && ast.getNextSibling() != null;
309         return isQualifiedIdent || isQualifiedIdentFromMethodRef;
310     }
311 
312     /**
313      * Collects the details of imports.
314      *
315      * @param ast node containing the import details
316      */
317     private void processImport(DetailAST ast) {
318         final FullIdent name = FullIdent.createFullIdentBelow(ast);
319         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
320             imports.add(name);
321         }
322     }
323 
324     /**
325      * Collects the details of static imports.
326      *
327      * @param ast node containing the static import details
328      */
329     private void processStaticImport(DetailAST ast) {
330         final FullIdent name =
331             FullIdent.createFullIdent(
332                 ast.getFirstChild().getNextSibling());
333         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
334             imports.add(name);
335         }
336     }
337 
338     /**
339      * Processes a Javadoc reference to record referenced types.
340      *
341      * @param ast the Javadoc reference node
342      */
343     private void processReference(DetailNode ast) {
344         final String referenceText = topLevelType(ast.getFirstChild().getText());
345         currentFrame.addReferencedType(referenceText);
346     }
347 
348     /**
349      * Processes a Javadoc parameter type tag to record referenced type.
350      *
351      * @param ast the Javadoc parameter type node
352      */
353     private void processParameterType(DetailNode ast) {
354         addReferencedTypesFromType(ast.getText());
355     }
356 
357     /**
358      * Registers all type names referenced in a type string.
359      * Handles generic type arguments, wildcard bounds, and array suffixes.
360      *
361      * @param type the type string to process
362      */
363     private void addReferencedTypesFromType(String type) {
364         String currentType = type;
365         if (currentType.startsWith(WILDCARD_EXTENDS_PREFIX)) {
366             currentType = currentType.substring(WILDCARD_EXTENDS_PREFIX.length());
367         }
368         else if (currentType.startsWith(WILDCARD_SUPER_PREFIX)) {
369             currentType = currentType.substring(WILDCARD_SUPER_PREFIX.length());
370         }
371         else {
372             currentType = stripTrailingParameterName(currentType);
373         }
374         if (currentType.endsWith("[]")) {
375             currentType = currentType.substring(0, currentType.length() - 2);
376         }
377         String outerType = stripTypeArguments(currentType);
378         outerType = stripTrailingGt(outerType);
379         outerType = topLevelType(outerType);
380         currentFrame.addReferencedType(outerType);
381         final int openIndex = currentType.indexOf('<');
382         if (openIndex != -1) {
383             final int closeIndex = findMatchingCloseAngle(currentType, openIndex);
384             if (closeIndex != -1) {
385                 final String typeArgs = currentType.substring(openIndex + 1, closeIndex);
386                 for (String arg : splitTypeArguments(typeArgs)) {
387                     addReferencedTypesFromType(arg);
388                 }
389             }
390         }
391     }
392 
393     /**
394      * Processes a Javadoc throws or exception tag to record referenced type.
395      *
396      * @param ast the Javadoc throws or exception node
397      */
398     private void processException(DetailNode ast) {
399         final DetailNode ident =
400                 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER);
401         if (ident != null) {
402             currentFrame.addReferencedType(ident.getText());
403         }
404     }
405 
406     /**
407      * If the given type string contains "." (e.g. "Map.Entry"), returns the
408      * top level type (e.g. "Map"), as that is what must be imported for the
409      * type to resolve. Otherwise, returns the type as-is.
410      *
411      * @param type A possibly qualified type name
412      * @return The simple name of the top level type
413      */
414     private static String topLevelType(String type) {
415         String result = type;
416         final int dotIndex = type.indexOf('.');
417         if (dotIndex != -1) {
418             result = type.substring(0, dotIndex);
419         }
420         return result;
421     }
422 
423     /**
424      * Strips generic type arguments from a type string.
425      *
426      * @param type A type string possibly containing type arguments
427      * @return The type string with type arguments removed
428      */
429     private static String stripTypeArguments(final String type) {
430         final int index = type.indexOf('<');
431         final String result;
432         if (index == -1) {
433             result = type;
434         }
435         else {
436             result = type.substring(0, index);
437         }
438         return result;
439     }
440 
441     /**
442      * Strips trailing {@code >} characters from a type string.
443      * This handles tokenization artifacts where the closing angle bracket
444      * of an enclosing generic is attached to the last parameter type.
445      *
446      * @param type A type string possibly ending with {@code >}
447      * @return The type string with trailing {@code >} characters removed
448      */
449     private static String stripTrailingGt(String type) {
450         String result = type;
451         while (result.endsWith(">")) {
452             result = result.substring(0, result.length() - 1);
453         }
454         return result;
455     }
456 
457     /**
458      * Strips a trailing parameter name (e.g. &quot;outputTarget&quot; in
459      * &quot;Result outputTarget&quot;) from a type token when the
460      * Javadoc lexer merges the type and parameter name into a
461      * single PARAMETER_TYPE token.
462      *
463      * <p>Only strips if the substring after the last space is a valid
464      * Java identifier, which is true for a parameter name but false
465      * for generic content such as {@code BigDecimal>} in
466      * {@code Class<? extends BigDecimal>}.</p>
467      *
468      * @param type the raw token text
469      * @return the type portion with any trailing parameter name removed
470      */
471     private static String stripTrailingParameterName(String type) {
472         final int lastSpace = type.lastIndexOf(' ');
473         String result = type;
474         if (lastSpace != -1) {
475             final String after = type.substring(lastSpace + 1);
476             if (PARAM_NAME_PATTERN.matcher(after).matches()) {
477                 result = type.substring(0, lastSpace);
478             }
479         }
480         return result;
481     }
482 
483     /**
484      * Finds the matching close angle bracket for the angle bracket at the given index.
485      * Correctly handles nested angle brackets by tracking depth.
486      *
487      * @param str the string to search in
488      * @param openIndex the index of the opening angle bracket
489      * @return the index of the matching close angle bracket, or -1 if not found
490      */
491     private static int findMatchingCloseAngle(String str, int openIndex) {
492         int depth = 0;
493         int result = -1;
494         for (int idx = openIndex; idx < str.length(); idx++) {
495             if (str.charAt(idx) == '<') {
496                 depth++;
497             }
498             else if (str.charAt(idx) == '>') {
499                 depth--;
500                 if (depth == 0) {
501                     result = idx;
502                     break;
503                 }
504             }
505         }
506         return result;
507     }
508 
509     /**
510      * Splits a type argument string into individual type argument strings.
511      * Comma handling is deferred to a follow-up issue that absorbs commas into
512      * PARAMETER_TYPE; currently commas are token boundaries so only one type
513      * argument ever appears here.
514      *
515      * @param typeArgs the type argument string (content between angle brackets)
516      * @return the list of individual type argument strings
517      */
518     private static List<String> splitTypeArguments(String typeArgs) {
519         final List<String> result = new ArrayList<>();
520         result.add(typeArgs);
521         return result;
522     }
523 
524     /**
525      * Holds the names of referenced types and names of declared inner types.
526      */
527     private static final class Frame {
528 
529         /** Parent frame. */
530         private final Frame parent;
531 
532         /** Nested types declared in the current scope. */
533         private final Set<String> declaredTypes;
534 
535         /** Set of references - possibly to imports or locally declared types. */
536         private final Set<String> referencedTypes;
537 
538         /**
539          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
540          *
541          * @param parent the parent frame
542          */
543         private Frame(Frame parent) {
544             this.parent = parent;
545             declaredTypes = new HashSet<>();
546             referencedTypes = new HashSet<>();
547         }
548 
549         /**
550          * Adds new inner type.
551          *
552          * @param type the type name
553          */
554         /* package */ void addDeclaredType(String type) {
555             declaredTypes.add(type);
556         }
557 
558         /**
559          * Adds new type reference to the current frame.
560          *
561          * @param type the type name
562          */
563         /* package */ void addReferencedType(String type) {
564             referencedTypes.add(type);
565         }
566 
567         /**
568          * Adds new inner types.
569          *
570          * @param types the type names
571          */
572         /* package */ void addReferencedTypes(Collection<String> types) {
573             referencedTypes.addAll(types);
574         }
575 
576         /**
577          * Filters out all references to locally defined types.
578          *
579          */
580         /* package */ void finish() {
581             referencedTypes.removeAll(declaredTypes);
582         }
583 
584         /**
585          * Creates new inner frame.
586          *
587          * @return a new frame.
588          */
589         /* package */ Frame push() {
590             return new Frame(this);
591         }
592 
593         /**
594          * Pulls all referenced types up, except those that are declared in this scope.
595          *
596          * @return the parent frame
597          */
598         /* package */ Frame pop() {
599             finish();
600             parent.addReferencedTypes(referencedTypes);
601             return parent;
602         }
603 
604         /**
605          * Checks whether this type name is used in this frame.
606          *
607          * @param type the type name
608          * @return {@code true} if the type is used
609          */
610         /* package */ boolean isReferencedType(String type) {
611             return referencedTypes.contains(type);
612         }
613 
614         /**
615          * Creates a new top-level frame for the compilation unit.
616          *
617          * @return a new frame.
618          */
619         /* package */ static Frame compilationUnit() {
620             return new Frame(null);
621         }
622 
623     }
624 
625 }