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