001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.imports;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
031import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.FileContents;
034import com.puppycrawl.tools.checkstyle.api.FullIdent;
035import com.puppycrawl.tools.checkstyle.api.TextBlock;
036import com.puppycrawl.tools.checkstyle.api.TokenTypes;
037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
038import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
040import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
041
042/**
043 * <p>
044 * Checks for unused import statements. Checkstyle uses a simple but very
045 * reliable algorithm to report on unused import statements. An import statement
046 * is considered unused if:
047 * </p>
048 * <ul>
049 * <li>
050 * It is not referenced in the file. The algorithm does not support wild-card
051 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
052 * checks for imports that handle wild-card imports.
053 * </li>
054 * <li>
055 * It is a duplicate of another import. This is when a class is imported more
056 * than once.
057 * </li>
058 * <li>
059 * The class imported is from the {@code java.lang} package. For example
060 * importing {@code java.lang.String}.
061 * </li>
062 * <li>
063 * The class imported is from the same package.
064 * </li>
065 * <li>
066 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
067 * default, but it is considered bad practice to introduce a compile-time
068 * dependency for documentation purposes only. As an example, the import
069 * {@code java.util.List} would be considered referenced with the Javadoc
070 * comment {@code {@link List}}. The alternative to avoid introducing a compile-time
071 * dependency would be to write the Javadoc comment as {@code {&#64;link java.util.List}}.
072 * </li>
073 * </ul>
074 * <p>
075 * The main limitation of this check is handling the cases where:
076 * </p>
077 * <ul>
078 * <li>
079 * An imported type has the same name as a declaration, such as a member variable.
080 * </li>
081 * <li>
082 * There are two or more imports with the same name.
083 * </li>
084 * </ul>
085 * <p>
086 * For example, in the following case all imports will not be flagged as unused:
087 * </p>
088 * <pre>
089 * import java.awt.Component;
090 * import static AstTreeStringPrinter.printFileAst;
091 * import static DetailNodeTreeStringPrinter.printFileAst;
092 * class FooBar {
093 *   private Object Component; // a bad practice in my opinion
094 *   void method() {
095 *       printFileAst(file); // two imports with the same name
096 *   }
097 * }
098 * </pre>
099 * <ul>
100 * <li>
101 * Property {@code processJavadoc} - Control whether to process Javadoc comments.
102 * Type is {@code boolean}.
103 * Default value is {@code true}.
104 * </li>
105 * </ul>
106 * <p>
107 * To configure the check:
108 * </p>
109 * <pre>
110 * &lt;module name="UnusedImports"/&gt;
111 * </pre>
112 * <p>
113 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
114 * </p>
115 * <p>
116 * Violation Message Keys:
117 * </p>
118 * <ul>
119 * <li>
120 * {@code import.unused}
121 * </li>
122 * </ul>
123 *
124 * @since 3.0
125 */
126@FileStatefulCheck
127public class UnusedImportsCheck extends AbstractCheck {
128
129    /**
130     * A key is pointing to the warning message text in "messages.properties"
131     * file.
132     */
133    public static final String MSG_KEY = "import.unused";
134
135    /** Regex to match class names. */
136    private static final Pattern CLASS_NAME = CommonUtil.createPattern(
137           "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
138    /** Regex to match the first class name. */
139    private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
140           "^" + CLASS_NAME);
141    /** Regex to match argument names. */
142    private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
143           "[(,]\\s*" + CLASS_NAME.pattern());
144
145    /** Regexp pattern to match java.lang package. */
146    private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
147        CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
148
149    /** Suffix for the star import. */
150    private static final String STAR_IMPORT_SUFFIX = ".*";
151
152    /** Set of the imports. */
153    private final Set<FullIdent> imports = new HashSet<>();
154
155    /** Flag to indicate when time to start collecting references. */
156    private boolean collect;
157    /** Control whether to process Javadoc comments. */
158    private boolean processJavadoc = true;
159
160    /**
161     * The scope is being processed.
162     * Types declared in a scope can shadow imported types.
163     */
164    private Frame currentFrame;
165
166    /**
167     * Setter to control whether to process Javadoc comments.
168     *
169     * @param value Flag for processing Javadoc comments.
170     */
171    public void setProcessJavadoc(boolean value) {
172        processJavadoc = value;
173    }
174
175    @Override
176    public void beginTree(DetailAST rootAST) {
177        collect = false;
178        currentFrame = Frame.compilationUnit();
179        imports.clear();
180    }
181
182    @Override
183    public void finishTree(DetailAST rootAST) {
184        currentFrame.finish();
185        // loop over all the imports to see if referenced.
186        imports.stream()
187            .filter(imprt -> isUnusedImport(imprt.getText()))
188            .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
189    }
190
191    @Override
192    public int[] getDefaultTokens() {
193        return getRequiredTokens();
194    }
195
196    @Override
197    public int[] getRequiredTokens() {
198        return new int[] {
199            TokenTypes.IDENT,
200            TokenTypes.IMPORT,
201            TokenTypes.STATIC_IMPORT,
202            // Definitions that may contain Javadoc...
203            TokenTypes.PACKAGE_DEF,
204            TokenTypes.ANNOTATION_DEF,
205            TokenTypes.ANNOTATION_FIELD_DEF,
206            TokenTypes.ENUM_DEF,
207            TokenTypes.ENUM_CONSTANT_DEF,
208            TokenTypes.CLASS_DEF,
209            TokenTypes.INTERFACE_DEF,
210            TokenTypes.METHOD_DEF,
211            TokenTypes.CTOR_DEF,
212            TokenTypes.VARIABLE_DEF,
213            TokenTypes.RECORD_DEF,
214            TokenTypes.COMPACT_CTOR_DEF,
215            // Tokens for creating a new frame
216            TokenTypes.OBJBLOCK,
217            TokenTypes.SLIST,
218        };
219    }
220
221    @Override
222    public int[] getAcceptableTokens() {
223        return getRequiredTokens();
224    }
225
226    @Override
227    public void visitToken(DetailAST ast) {
228        switch (ast.getType()) {
229            case TokenTypes.IDENT:
230                if (collect) {
231                    processIdent(ast);
232                }
233                break;
234            case TokenTypes.IMPORT:
235                processImport(ast);
236                break;
237            case TokenTypes.STATIC_IMPORT:
238                processStaticImport(ast);
239                break;
240            case TokenTypes.OBJBLOCK:
241            case TokenTypes.SLIST:
242                currentFrame = currentFrame.push();
243                break;
244            default:
245                collect = true;
246                if (processJavadoc) {
247                    collectReferencesFromJavadoc(ast);
248                }
249                break;
250        }
251    }
252
253    @Override
254    public void leaveToken(DetailAST ast) {
255        if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
256            currentFrame = currentFrame.pop();
257        }
258    }
259
260    /**
261     * Checks whether an import is unused.
262     *
263     * @param imprt an import.
264     * @return true if an import is unused.
265     */
266    private boolean isUnusedImport(String imprt) {
267        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
268        return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
269            || javaLangPackageMatcher.matches();
270    }
271
272    /**
273     * Collects references made by IDENT.
274     *
275     * @param ast the IDENT node to process
276     */
277    private void processIdent(DetailAST ast) {
278        final DetailAST parent = ast.getParent();
279        final int parentType = parent.getType();
280
281        final boolean isPossibleDotClassOrInMethod = parentType == TokenTypes.DOT
282                || parentType == TokenTypes.METHOD_DEF;
283
284        final boolean isQualifiedIdent = parentType == TokenTypes.DOT
285                && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
286                && ast.getNextSibling() != null;
287
288        if (TokenUtil.isTypeDeclaration(parentType)) {
289            currentFrame.addDeclaredType(ast.getText());
290        }
291        else if (!isPossibleDotClassOrInMethod || isQualifiedIdent) {
292            currentFrame.addReferencedType(ast.getText());
293        }
294    }
295
296    /**
297     * Collects the details of imports.
298     *
299     * @param ast node containing the import details
300     */
301    private void processImport(DetailAST ast) {
302        final FullIdent name = FullIdent.createFullIdentBelow(ast);
303        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
304            imports.add(name);
305        }
306    }
307
308    /**
309     * Collects the details of static imports.
310     *
311     * @param ast node containing the static import details
312     */
313    private void processStaticImport(DetailAST ast) {
314        final FullIdent name =
315            FullIdent.createFullIdent(
316                ast.getFirstChild().getNextSibling());
317        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
318            imports.add(name);
319        }
320    }
321
322    /**
323     * Collects references made in Javadoc comments.
324     *
325     * @param ast node to inspect for Javadoc
326     */
327    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
328    @SuppressWarnings("deprecation")
329    private void collectReferencesFromJavadoc(DetailAST ast) {
330        final FileContents contents = getFileContents();
331        final int lineNo = ast.getLineNo();
332        final TextBlock textBlock = contents.getJavadocBefore(lineNo);
333        if (textBlock != null) {
334            currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
335        }
336    }
337
338    /**
339     * Process a javadoc {@link TextBlock} and return the set of classes
340     * referenced within.
341     *
342     * @param textBlock The javadoc block to parse
343     * @return a set of classes referenced in the javadoc block
344     */
345    private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
346        final List<JavadocTag> tags = new ArrayList<>();
347        // gather all the inline tags, like @link
348        // INLINE tags inside BLOCKs get hidden when using ALL
349        tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE));
350        // gather all the block-level tags, like @throws and @see
351        tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK));
352
353        final Set<String> references = new HashSet<>();
354
355        tags.stream()
356            .filter(JavadocTag::canReferenceImports)
357            .forEach(tag -> references.addAll(processJavadocTag(tag)));
358        return references;
359    }
360
361    /**
362     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
363     *
364     * @param cmt The javadoc block to parse
365     * @param tagType The type of tags we're interested in
366     * @return the list of tags
367     */
368    private static List<JavadocTag> getValidTags(TextBlock cmt,
369            JavadocUtil.JavadocTagType tagType) {
370        return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags();
371    }
372
373    /**
374     * Returns a list of references that found in a javadoc {@link JavadocTag}.
375     *
376     * @param tag The javadoc tag to parse
377     * @return A list of references that found in this tag
378     */
379    private static Set<String> processJavadocTag(JavadocTag tag) {
380        final Set<String> references = new HashSet<>();
381        final String identifier = tag.getFirstArg().trim();
382        for (Pattern pattern : new Pattern[]
383        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
384            references.addAll(matchPattern(identifier, pattern));
385        }
386        return references;
387    }
388
389    /**
390     * Extracts a set of texts matching a {@link Pattern} from a
391     * {@link String}.
392     *
393     * @param identifier The String to match the pattern against
394     * @param pattern The Pattern used to extract the texts
395     * @return A set of texts which matched the pattern
396     */
397    private static Set<String> matchPattern(String identifier, Pattern pattern) {
398        final Set<String> references = new HashSet<>();
399        final Matcher matcher = pattern.matcher(identifier);
400        while (matcher.find()) {
401            references.add(topLevelType(matcher.group(1)));
402        }
403        return references;
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        final String topLevelType;
416        final int dotIndex = type.indexOf('.');
417        if (dotIndex == -1) {
418            topLevelType = type;
419        }
420        else {
421            topLevelType = type.substring(0, dotIndex);
422        }
423        return topLevelType;
424    }
425
426    /**
427     * Holds the names of referenced types and names of declared inner types.
428     */
429    private static final class Frame {
430
431        /** Parent frame. */
432        private final Frame parent;
433
434        /** Nested types declared in the current scope. */
435        private final Set<String> declaredTypes;
436
437        /** Set of references - possibly to imports or locally declared types. */
438        private final Set<String> referencedTypes;
439
440        /**
441         * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
442         *
443         * @param parent the parent frame
444         */
445        private Frame(Frame parent) {
446            this.parent = parent;
447            declaredTypes = new HashSet<>();
448            referencedTypes = new HashSet<>();
449        }
450
451        /**
452         * Adds new inner type.
453         *
454         * @param type the type name
455         */
456        public void addDeclaredType(String type) {
457            declaredTypes.add(type);
458        }
459
460        /**
461         * Adds new type reference to the current frame.
462         *
463         * @param type the type name
464         */
465        public void addReferencedType(String type) {
466            referencedTypes.add(type);
467        }
468
469        /**
470         * Adds new inner types.
471         *
472         * @param types the type names
473         */
474        public void addReferencedTypes(Collection<String> types) {
475            referencedTypes.addAll(types);
476        }
477
478        /**
479         * Filters out all references to locally defined types.
480         *
481         */
482        public void finish() {
483            referencedTypes.removeAll(declaredTypes);
484        }
485
486        /**
487         * Creates new inner frame.
488         *
489         * @return a new frame.
490         */
491        public Frame push() {
492            return new Frame(this);
493        }
494
495        /**
496         * Pulls all referenced types up, except those that are declared in this scope.
497         *
498         * @return the parent frame
499         */
500        public Frame pop() {
501            finish();
502            parent.addReferencedTypes(referencedTypes);
503            return parent;
504        }
505
506        /**
507         * Checks whether this type name is used in this frame.
508         *
509         * @param type the type name
510         * @return {@code true} if the type is used
511         */
512        public boolean isReferencedType(String type) {
513            return referencedTypes.contains(type);
514        }
515
516        /**
517         * Creates a new top-level frame for the compilation unit.
518         *
519         * @return a new frame.
520         */
521        public static Frame compilationUnit() {
522            return new Frame(null);
523        }
524
525    }
526
527}