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.Set;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.DetailNode;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
33  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
35  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
36  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
37  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
38  
39  /**
40   * <div>
41   * Checks for unused import statements. An import statement
42   * is considered unused if:
43   * </div>
44   *
45   * <ul>
46   * <li>
47   * It is not referenced in the file. The algorithm does not support wild-card
48   * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
49   * checks for imports that handle wild-card imports.
50   * </li>
51   * <li>
52   * The class imported is from the {@code java.lang} package. For example
53   * importing {@code java.lang.String}.
54   * </li>
55   * <li>
56   * The class imported is from the same package.
57   * </li>
58   * <li>
59   * A static method is imported when used as method reference. In that case,
60   * only the type needs to be imported and that's enough to resolve the method.
61   * </li>
62   * <li>
63   * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
64   * default, but it is considered bad practice to introduce a compile-time
65   * dependency for documentation purposes only. As an example, the import
66   * {@code java.util.Set} would be considered referenced with the Javadoc
67   * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time
68   * dependency would be to write the Javadoc comment as {@code {@link Set}}.
69   * </li>
70   * </ul>
71   *
72   * <p>
73   * The main limitation of this check is handling the cases where:
74   * </p>
75   * <ul>
76   * <li>
77   * An imported type has the same name as a declaration, such as a member variable.
78   * </li>
79   * <li>
80   * There are two or more static imports with the same method name
81   * (javac can distinguish imports with same name but different parameters, but checkstyle can not
82   * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
83   * </li>
84   * <li>
85   * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
86   * determine which packages or types are brought into scope by an {@code import module} declaration.
87   * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
88   * </li>
89   * </ul>
90   *
91   * @since 3.0
92   */
93  @FileStatefulCheck
94  @SuppressWarnings("UnrecognisedJavadocTag")
95  public class UnusedImportsCheck extends AbstractJavadocCheck {
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     /** Regexp pattern to match java.lang package. */
104     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
105         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
106 
107     /** Suffix for the star import. */
108     private static final String STAR_IMPORT_SUFFIX = ".*";
109 
110     /** Set of the imports. */
111     private final Set<FullIdent> imports = new HashSet<>();
112 
113     /** Control whether to process Javadoc comments. */
114     private boolean processJavadoc = true;
115 
116     /**
117      * The scope is being processed.
118      * Types declared in a scope can shadow imported types.
119      */
120     private Frame currentFrame;
121 
122     /**
123      * Setter to control whether to process Javadoc comments.
124      *
125      * @param value Flag for processing Javadoc comments.
126      * @since 5.4
127      */
128     public void setProcessJavadoc(boolean value) {
129         processJavadoc = value;
130     }
131 
132     /**
133      * Setter to control when to print violations if the Javadoc being examined by this check
134      * violates the tight html rules defined at
135      * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
136      *     Tight-HTML Rules</a>.
137      *
138      * @param shouldReportViolation value to which the field shall be set to
139      * @since 8.3
140      * @propertySince 13.4.0
141      */
142     @Override
143     public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
144         super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
145     }
146 
147     @Override
148     public void beginTree(DetailAST rootAST) {
149         super.beginTree(rootAST);
150         currentFrame = Frame.compilationUnit();
151         imports.clear();
152     }
153 
154     @Override
155     public void finishTree(DetailAST rootAST) {
156         currentFrame.finish();
157         // loop over all the imports to see if referenced.
158         imports.stream()
159             .filter(imprt -> isUnusedImport(imprt.getText()))
160             .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
161     }
162 
163     @Override
164     public int[] getRequiredJavadocTokens() {
165         return new int[] {
166             JavadocCommentsTokenTypes.REFERENCE,
167             JavadocCommentsTokenTypes.PARAMETER_TYPE,
168             JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
169             JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
170         };
171     }
172 
173     @Override
174     public int[] getDefaultJavadocTokens() {
175         return getRequiredJavadocTokens();
176     }
177 
178     @Override
179     public void visitJavadocToken(DetailNode ast) {
180         switch (ast.getType()) {
181             case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast);
182             case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast);
183             case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
184                  JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast);
185             default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
186         }
187 
188     }
189 
190     @Override
191     public int[] getDefaultTokens() {
192         return getRequiredTokens();
193     }
194 
195     @Override
196     public int[] getAcceptableTokens() {
197         return getRequiredTokens();
198     }
199 
200     @Override
201     public int[] getRequiredTokens() {
202         return new int[] {
203             TokenTypes.IDENT,
204             TokenTypes.IMPORT,
205             TokenTypes.STATIC_IMPORT,
206             // Tokens for creating a new frame
207             TokenTypes.OBJBLOCK,
208             TokenTypes.SLIST,
209             // Javadoc
210             TokenTypes.BLOCK_COMMENT_BEGIN,
211         };
212     }
213 
214     @Override
215     public void visitToken(DetailAST ast) {
216         switch (ast.getType()) {
217             case TokenTypes.IDENT -> processIdent(ast);
218             case TokenTypes.IMPORT -> processImport(ast);
219             case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
220             case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
221             case TokenTypes.BLOCK_COMMENT_BEGIN -> {
222                 if (processJavadoc) {
223                     super.visitToken(ast);
224                 }
225             }
226             default -> throw new IllegalArgumentException("Unknown token type " + ast);
227         }
228     }
229 
230     @Override
231     public void leaveToken(DetailAST ast) {
232         if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
233             currentFrame = currentFrame.pop();
234         }
235     }
236 
237     /**
238      * Checks whether an import is unused.
239      *
240      * @param imprt an import.
241      * @return true if an import is unused.
242      */
243     private boolean isUnusedImport(String imprt) {
244         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
245         return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
246             || javaLangPackageMatcher.matches();
247     }
248 
249     /**
250      * Collects references made by IDENT.
251      *
252      * @param ast the IDENT node to process
253      */
254     private void processIdent(DetailAST ast) {
255         final DetailAST parent = ast.getParent();
256         final int parentType = parent.getType();
257 
258         // Ignore IDENTs that are part of the import statement itself
259         final boolean collect = parentType != TokenTypes.IMPORT
260                 && parentType != TokenTypes.STATIC_IMPORT;
261 
262         if (collect) {
263             final boolean isClassOrMethod = parentType == TokenTypes.DOT
264                 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
265 
266             if (TokenUtil.isTypeDeclaration(parentType)) {
267                 currentFrame.addDeclaredType(ast.getText());
268             }
269             else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
270                 currentFrame.addReferencedType(ast.getText());
271             }
272         }
273     }
274 
275     /**
276      * Checks whether ast is a fully qualified identifier.
277      *
278      * @param ast to check
279      * @return true if given ast is a fully qualified identifier
280      */
281     private static boolean isQualifiedIdentifier(DetailAST ast) {
282         final DetailAST parent = ast.getParent();
283         final int parentType = parent.getType();
284 
285         final boolean isQualifiedIdent = parentType == TokenTypes.DOT
286                 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
287                 && ast.getNextSibling() != null;
288         final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
289                 && ast.getNextSibling() != null;
290         return isQualifiedIdent || isQualifiedIdentFromMethodRef;
291     }
292 
293     /**
294      * Collects the details of imports.
295      *
296      * @param ast node containing the import details
297      */
298     private void processImport(DetailAST ast) {
299         final FullIdent name = FullIdent.createFullIdentBelow(ast);
300         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
301             imports.add(name);
302         }
303     }
304 
305     /**
306      * Collects the details of static imports.
307      *
308      * @param ast node containing the static import details
309      */
310     private void processStaticImport(DetailAST ast) {
311         final FullIdent name =
312             FullIdent.createFullIdent(
313                 ast.getFirstChild().getNextSibling());
314         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
315             imports.add(name);
316         }
317     }
318 
319     /**
320      * Processes a Javadoc reference to record referenced types.
321      *
322      * @param ast the Javadoc reference node
323      */
324     private void processReference(DetailNode ast) {
325         final String referenceText = topLevelType(ast.getFirstChild().getText());
326         currentFrame.addReferencedType(referenceText);
327     }
328 
329     /**
330      * Processes a Javadoc parameter type tag to record referenced type.
331      *
332      * @param ast the Javadoc parameter type node
333      */
334     private void processParameterType(DetailNode ast) {
335         String parameterTypeText = topLevelType(ast.getText());
336         if (parameterTypeText.endsWith("[]")) {
337             parameterTypeText = parameterTypeText.substring(0, parameterTypeText.length() - 2);
338         }
339         currentFrame.addReferencedType(parameterTypeText);
340     }
341 
342     /**
343      * Processes a Javadoc throws or exception tag to record referenced type.
344      *
345      * @param ast the Javadoc throws or exception node
346      */
347     private void processException(DetailNode ast) {
348         final DetailNode ident =
349                 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER);
350         if (ident != null) {
351             currentFrame.addReferencedType(ident.getText());
352         }
353     }
354 
355     /**
356      * If the given type string contains "." (e.g. "Map.Entry"), returns the
357      * top level type (e.g. "Map"), as that is what must be imported for the
358      * type to resolve. Otherwise, returns the type as-is.
359      *
360      * @param type A possibly qualified type name
361      * @return The simple name of the top level type
362      */
363     private static String topLevelType(String type) {
364         String result = type;
365         final int dotIndex = type.indexOf('.');
366         if (dotIndex != -1) {
367             result = type.substring(0, dotIndex);
368         }
369         return result;
370     }
371 
372     /**
373      * Holds the names of referenced types and names of declared inner types.
374      */
375     private static final class Frame {
376 
377         /** Parent frame. */
378         private final Frame parent;
379 
380         /** Nested types declared in the current scope. */
381         private final Set<String> declaredTypes;
382 
383         /** Set of references - possibly to imports or locally declared types. */
384         private final Set<String> referencedTypes;
385 
386         /**
387          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
388          *
389          * @param parent the parent frame
390          */
391         private Frame(Frame parent) {
392             this.parent = parent;
393             declaredTypes = new HashSet<>();
394             referencedTypes = new HashSet<>();
395         }
396 
397         /**
398          * Adds new inner type.
399          *
400          * @param type the type name
401          */
402         /* package */ void addDeclaredType(String type) {
403             declaredTypes.add(type);
404         }
405 
406         /**
407          * Adds new type reference to the current frame.
408          *
409          * @param type the type name
410          */
411         /* package */ void addReferencedType(String type) {
412             referencedTypes.add(type);
413         }
414 
415         /**
416          * Adds new inner types.
417          *
418          * @param types the type names
419          */
420         /* package */ void addReferencedTypes(Collection<String> types) {
421             referencedTypes.addAll(types);
422         }
423 
424         /**
425          * Filters out all references to locally defined types.
426          *
427          */
428         /* package */ void finish() {
429             referencedTypes.removeAll(declaredTypes);
430         }
431 
432         /**
433          * Creates new inner frame.
434          *
435          * @return a new frame.
436          */
437         /* package */ Frame push() {
438             return new Frame(this);
439         }
440 
441         /**
442          * Pulls all referenced types up, except those that are declared in this scope.
443          *
444          * @return the parent frame
445          */
446         /* package */ Frame pop() {
447             finish();
448             parent.addReferencedTypes(referencedTypes);
449             return parent;
450         }
451 
452         /**
453          * Checks whether this type name is used in this frame.
454          *
455          * @param type the type name
456          * @return {@code true} if the type is used
457          */
458         /* package */ boolean isReferencedType(String type) {
459             return referencedTypes.contains(type);
460         }
461 
462         /**
463          * Creates a new top-level frame for the compilation unit.
464          *
465          * @return a new frame.
466          */
467         /* package */ static Frame compilationUnit() {
468             return new Frame(null);
469         }
470 
471     }
472 
473 }