View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.AbstractCheck;
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.FileContents;
34  import com.puppycrawl.tools.checkstyle.api.FullIdent;
35  import com.puppycrawl.tools.checkstyle.api.TextBlock;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
40  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
41  
42  /**
43   * <p>
44   * Checks for unused import statements. An import statement
45   * is considered unused if:
46   * </p>
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.List} would be considered referenced with the Javadoc
69   * comment {@code {@link List}}. The alternative to avoid introducing a compile-time
70   * dependency would be to write the Javadoc comment as {@code {&#64;link java.util.List}}.
71   * </li>
72   * </ul>
73   * <p>
74   * The main limitation of this check is handling the cases where:
75   * </p>
76   * <ul>
77   * <li>
78   * An imported type has the same name as a declaration, such as a member variable.
79   * </li>
80   * <li>
81   * There are two or more static imports with the same method name
82   * (javac can distinguish imports with same name but different parameters, but checkstyle can not
83   * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
84   * </li>
85   * </ul>
86   * <ul>
87   * <li>
88   * Property {@code processJavadoc} - Control whether to process Javadoc comments.
89   * Type is {@code boolean}.
90   * Default value is {@code true}.
91   * </li>
92   * </ul>
93   * <p>
94   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
95   * </p>
96   * <p>
97   * Violation Message Keys:
98   * </p>
99   * <ul>
100  * <li>
101  * {@code import.unused}
102  * </li>
103  * </ul>
104  *
105  * @since 3.0
106  */
107 @FileStatefulCheck
108 public class UnusedImportsCheck extends AbstractCheck {
109 
110     /**
111      * A key is pointing to the warning message text in "messages.properties"
112      * file.
113      */
114     public static final String MSG_KEY = "import.unused";
115 
116     /** Regex to match class names. */
117     private static final Pattern CLASS_NAME = CommonUtil.createPattern(
118            "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
119     /** Regex to match the first class name. */
120     private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
121            "^" + CLASS_NAME);
122     /** Regex to match argument names. */
123     private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
124            "[(,]\\s*" + CLASS_NAME.pattern());
125 
126     /** Regexp pattern to match java.lang package. */
127     private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
128         CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
129 
130     /** Suffix for the star import. */
131     private static final String STAR_IMPORT_SUFFIX = ".*";
132 
133     /** Set of the imports. */
134     private final Set<FullIdent> imports = new HashSet<>();
135 
136     /** Flag to indicate when time to start collecting references. */
137     private boolean collect;
138     /** Control whether to process Javadoc comments. */
139     private boolean processJavadoc = true;
140 
141     /**
142      * The scope is being processed.
143      * Types declared in a scope can shadow imported types.
144      */
145     private Frame currentFrame;
146 
147     /**
148      * Setter to control whether to process Javadoc comments.
149      *
150      * @param value Flag for processing Javadoc comments.
151      * @since 5.4
152      */
153     public void setProcessJavadoc(boolean value) {
154         processJavadoc = value;
155     }
156 
157     @Override
158     public void beginTree(DetailAST rootAST) {
159         collect = false;
160         currentFrame = Frame.compilationUnit();
161         imports.clear();
162     }
163 
164     @Override
165     public void finishTree(DetailAST rootAST) {
166         currentFrame.finish();
167         // loop over all the imports to see if referenced.
168         imports.stream()
169             .filter(imprt -> isUnusedImport(imprt.getText()))
170             .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
171     }
172 
173     @Override
174     public int[] getDefaultTokens() {
175         return getRequiredTokens();
176     }
177 
178     @Override
179     public int[] getRequiredTokens() {
180         return new int[] {
181             TokenTypes.IDENT,
182             TokenTypes.IMPORT,
183             TokenTypes.STATIC_IMPORT,
184             // Definitions that may contain Javadoc...
185             TokenTypes.PACKAGE_DEF,
186             TokenTypes.ANNOTATION_DEF,
187             TokenTypes.ANNOTATION_FIELD_DEF,
188             TokenTypes.ENUM_DEF,
189             TokenTypes.ENUM_CONSTANT_DEF,
190             TokenTypes.CLASS_DEF,
191             TokenTypes.INTERFACE_DEF,
192             TokenTypes.METHOD_DEF,
193             TokenTypes.CTOR_DEF,
194             TokenTypes.VARIABLE_DEF,
195             TokenTypes.RECORD_DEF,
196             TokenTypes.COMPACT_CTOR_DEF,
197             // Tokens for creating a new frame
198             TokenTypes.OBJBLOCK,
199             TokenTypes.SLIST,
200         };
201     }
202 
203     @Override
204     public int[] getAcceptableTokens() {
205         return getRequiredTokens();
206     }
207 
208     @Override
209     public void visitToken(DetailAST ast) {
210         switch (ast.getType()) {
211             case TokenTypes.IDENT:
212                 if (collect) {
213                     processIdent(ast);
214                 }
215                 break;
216             case TokenTypes.IMPORT:
217                 processImport(ast);
218                 break;
219             case TokenTypes.STATIC_IMPORT:
220                 processStaticImport(ast);
221                 break;
222             case TokenTypes.OBJBLOCK:
223             case TokenTypes.SLIST:
224                 currentFrame = currentFrame.push();
225                 break;
226             default:
227                 collect = true;
228                 if (processJavadoc) {
229                     collectReferencesFromJavadoc(ast);
230                 }
231                 break;
232         }
233     }
234 
235     @Override
236     public void leaveToken(DetailAST ast) {
237         if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
238             currentFrame = currentFrame.pop();
239         }
240     }
241 
242     /**
243      * Checks whether an import is unused.
244      *
245      * @param imprt an import.
246      * @return true if an import is unused.
247      */
248     private boolean isUnusedImport(String imprt) {
249         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
250         return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
251             || javaLangPackageMatcher.matches();
252     }
253 
254     /**
255      * Collects references made by IDENT.
256      *
257      * @param ast the IDENT node to process
258      */
259     private void processIdent(DetailAST ast) {
260         final DetailAST parent = ast.getParent();
261         final int parentType = parent.getType();
262 
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      * Checks whether ast is a fully qualified identifier.
276      *
277      * @param ast to check
278      * @return true if given ast is a fully qualified identifier
279      */
280     private static boolean isQualifiedIdentifier(DetailAST ast) {
281         final DetailAST parent = ast.getParent();
282         final int parentType = parent.getType();
283 
284         final boolean isQualifiedIdent = parentType == TokenTypes.DOT
285                 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
286                 && ast.getNextSibling() != null;
287         final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
288                 && ast.getNextSibling() != null;
289         return isQualifiedIdent || isQualifiedIdentFromMethodRef;
290     }
291 
292     /**
293      * Collects the details of imports.
294      *
295      * @param ast node containing the import details
296      */
297     private void processImport(DetailAST ast) {
298         final FullIdent name = FullIdent.createFullIdentBelow(ast);
299         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
300             imports.add(name);
301         }
302     }
303 
304     /**
305      * Collects the details of static imports.
306      *
307      * @param ast node containing the static import details
308      */
309     private void processStaticImport(DetailAST ast) {
310         final FullIdent name =
311             FullIdent.createFullIdent(
312                 ast.getFirstChild().getNextSibling());
313         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
314             imports.add(name);
315         }
316     }
317 
318     /**
319      * Collects references made in Javadoc comments.
320      *
321      * @param ast node to inspect for Javadoc
322      */
323     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
324     @SuppressWarnings("deprecation")
325     private void collectReferencesFromJavadoc(DetailAST ast) {
326         final FileContents contents = getFileContents();
327         final int lineNo = ast.getLineNo();
328         final TextBlock textBlock = contents.getJavadocBefore(lineNo);
329         if (textBlock != null) {
330             currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
331         }
332     }
333 
334     /**
335      * Process a javadoc {@link TextBlock} and return the set of classes
336      * referenced within.
337      *
338      * @param textBlock The javadoc block to parse
339      * @return a set of classes referenced in the javadoc block
340      */
341     private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
342         final List<JavadocTag> tags = new ArrayList<>();
343         // gather all the inline tags, like @link
344         // INLINE tags inside BLOCKs get hidden when using ALL
345         tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE));
346         // gather all the block-level tags, like @throws and @see
347         tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK));
348 
349         final Set<String> references = new HashSet<>();
350 
351         tags.stream()
352             .filter(JavadocTag::canReferenceImports)
353             .forEach(tag -> references.addAll(processJavadocTag(tag)));
354         return references;
355     }
356 
357     /**
358      * Returns the list of valid tags found in a javadoc {@link TextBlock}.
359      *
360      * @param cmt The javadoc block to parse
361      * @param tagType The type of tags we're interested in
362      * @return the list of tags
363      */
364     private static List<JavadocTag> getValidTags(TextBlock cmt,
365             JavadocUtil.JavadocTagType tagType) {
366         return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags();
367     }
368 
369     /**
370      * Returns a list of references that found in a javadoc {@link JavadocTag}.
371      *
372      * @param tag The javadoc tag to parse
373      * @return A list of references that found in this tag
374      */
375     private static Set<String> processJavadocTag(JavadocTag tag) {
376         final Set<String> references = new HashSet<>();
377         final String identifier = tag.getFirstArg();
378         for (Pattern pattern : new Pattern[]
379         {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
380             references.addAll(matchPattern(identifier, pattern));
381         }
382         return references;
383     }
384 
385     /**
386      * Extracts a set of texts matching a {@link Pattern} from a
387      * {@link String}.
388      *
389      * @param identifier The String to match the pattern against
390      * @param pattern The Pattern used to extract the texts
391      * @return A set of texts which matched the pattern
392      */
393     private static Set<String> matchPattern(String identifier, Pattern pattern) {
394         final Set<String> references = new HashSet<>();
395         final Matcher matcher = pattern.matcher(identifier);
396         while (matcher.find()) {
397             references.add(topLevelType(matcher.group(1)));
398         }
399         return references;
400     }
401 
402     /**
403      * If the given type string contains "." (e.g. "Map.Entry"), returns the
404      * top level type (e.g. "Map"), as that is what must be imported for the
405      * type to resolve. Otherwise, returns the type as-is.
406      *
407      * @param type A possibly qualified type name
408      * @return The simple name of the top level type
409      */
410     private static String topLevelType(String type) {
411         final String topLevelType;
412         final int dotIndex = type.indexOf('.');
413         if (dotIndex == -1) {
414             topLevelType = type;
415         }
416         else {
417             topLevelType = type.substring(0, dotIndex);
418         }
419         return topLevelType;
420     }
421 
422     /**
423      * Holds the names of referenced types and names of declared inner types.
424      */
425     private static final class Frame {
426 
427         /** Parent frame. */
428         private final Frame parent;
429 
430         /** Nested types declared in the current scope. */
431         private final Set<String> declaredTypes;
432 
433         /** Set of references - possibly to imports or locally declared types. */
434         private final Set<String> referencedTypes;
435 
436         /**
437          * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
438          *
439          * @param parent the parent frame
440          */
441         private Frame(Frame parent) {
442             this.parent = parent;
443             declaredTypes = new HashSet<>();
444             referencedTypes = new HashSet<>();
445         }
446 
447         /**
448          * Adds new inner type.
449          *
450          * @param type the type name
451          */
452         public void addDeclaredType(String type) {
453             declaredTypes.add(type);
454         }
455 
456         /**
457          * Adds new type reference to the current frame.
458          *
459          * @param type the type name
460          */
461         public void addReferencedType(String type) {
462             referencedTypes.add(type);
463         }
464 
465         /**
466          * Adds new inner types.
467          *
468          * @param types the type names
469          */
470         public void addReferencedTypes(Collection<String> types) {
471             referencedTypes.addAll(types);
472         }
473 
474         /**
475          * Filters out all references to locally defined types.
476          *
477          */
478         public void finish() {
479             referencedTypes.removeAll(declaredTypes);
480         }
481 
482         /**
483          * Creates new inner frame.
484          *
485          * @return a new frame.
486          */
487         public Frame push() {
488             return new Frame(this);
489         }
490 
491         /**
492          * Pulls all referenced types up, except those that are declared in this scope.
493          *
494          * @return the parent frame
495          */
496         public Frame pop() {
497             finish();
498             parent.addReferencedTypes(referencedTypes);
499             return parent;
500         }
501 
502         /**
503          * Checks whether this type name is used in this frame.
504          *
505          * @param type the type name
506          * @return {@code true} if the type is used
507          */
508         public boolean isReferencedType(String type) {
509             return referencedTypes.contains(type);
510         }
511 
512         /**
513          * Creates a new top-level frame for the compilation unit.
514          *
515          * @return a new frame.
516          */
517         public static Frame compilationUnit() {
518             return new Frame(null);
519         }
520 
521     }
522 
523 }