001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.HashSet;
024import java.util.List;
025import java.util.Set;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.FileContents;
033import com.puppycrawl.tools.checkstyle.api.FullIdent;
034import com.puppycrawl.tools.checkstyle.api.TextBlock;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
038import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
039
040/**
041 * <p>
042 * Checks for unused import statements. Checkstyle uses a simple but very
043 * reliable algorithm to report on unused import statements. An import statement
044 * is considered unused if:
045 * </p>
046 * <ul>
047 * <li>
048 * It is not referenced in the file. The algorithm does not support wild-card
049 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
050 * checks for imports that handle wild-card imports.
051 * </li>
052 * <li>
053 * It is a duplicate of another import. This is when a class is imported more
054 * than once.
055 * </li>
056 * <li>
057 * The class imported is from the {@code java.lang} package. For example
058 * importing {@code java.lang.String}.
059 * </li>
060 * <li>
061 * The class imported is from the same package.
062 * </li>
063 * <li>
064 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
065 * default, but it is considered bad practice to introduce a compile time
066 * dependency for documentation purposes only. As an example, the import
067 * {@code java.util.List} would be considered referenced with the Javadoc
068 * comment {@code {@link List}}. The alternative to avoid introducing a compile
069 * time dependency would be to write the Javadoc comment as {@code {&#64;link java.util.List}}.
070 * </li>
071 * </ul>
072 * <p>
073 * The main limitation of this check is handling the case where an imported type
074 * has the same name as a declaration, such as a member variable.
075 * </p>
076 * <p>
077 * For example, in the following case the import {@code java.awt.Component}
078 * will not be flagged as unused:
079 * </p>
080 * <pre>
081 * import java.awt.Component;
082 * class FooBar {
083 *   private Object Component; // a bad practice in my opinion
084 *   ...
085 * }
086 * </pre>
087 * <ul>
088 * <li>
089 * Property {@code processJavadoc} - Control whether to process Javadoc comments.
090 * Type is {@code boolean}.
091 * Default value is {@code true}.
092 * </li>
093 * </ul>
094 * <p>
095 * To configure the check:
096 * </p>
097 * <pre>
098 * &lt;module name="UnusedImports"/&gt;
099 * </pre>
100 * <p>
101 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
102 * </p>
103 * <p>
104 * Violation Message Keys:
105 * </p>
106 * <ul>
107 * <li>
108 * {@code import.unused}
109 * </li>
110 * </ul>
111 *
112 * @since 3.0
113 */
114@FileStatefulCheck
115public class UnusedImportsCheck extends AbstractCheck {
116
117    /**
118     * A key is pointing to the warning message text in "messages.properties"
119     * file.
120     */
121    public static final String MSG_KEY = "import.unused";
122
123    /** Regex to match class names. */
124    private static final Pattern CLASS_NAME = CommonUtil.createPattern(
125           "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
126    /** Regex to match the first class name. */
127    private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
128           "^" + CLASS_NAME);
129    /** Regex to match argument names. */
130    private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
131           "[(,]\\s*" + CLASS_NAME.pattern());
132
133    /** Regexp pattern to match java.lang package. */
134    private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
135        CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
136
137    /** Suffix for the star import. */
138    private static final String STAR_IMPORT_SUFFIX = ".*";
139
140    /** Set of the imports. */
141    private final Set<FullIdent> imports = new HashSet<>();
142
143    /** Set of references - possibly to imports or other things. */
144    private final Set<String> referenced = new HashSet<>();
145
146    /** Flag to indicate when time to start collecting references. */
147    private boolean collect;
148    /** Control whether to process Javadoc comments. */
149    private boolean processJavadoc = true;
150
151    /**
152     * Setter to control whether to process Javadoc comments.
153     *
154     * @param value Flag for processing Javadoc comments.
155     */
156    public void setProcessJavadoc(boolean value) {
157        processJavadoc = value;
158    }
159
160    @Override
161    public void beginTree(DetailAST rootAST) {
162        collect = false;
163        imports.clear();
164        referenced.clear();
165    }
166
167    @Override
168    public void finishTree(DetailAST rootAST) {
169        // loop over all the imports to see if referenced.
170        imports.stream()
171            .filter(imprt -> isUnusedImport(imprt.getText()))
172            .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
173    }
174
175    @Override
176    public int[] getDefaultTokens() {
177        return getRequiredTokens();
178    }
179
180    @Override
181    public int[] getRequiredTokens() {
182        return new int[] {
183            TokenTypes.IDENT,
184            TokenTypes.IMPORT,
185            TokenTypes.STATIC_IMPORT,
186            // Definitions that may contain Javadoc...
187            TokenTypes.PACKAGE_DEF,
188            TokenTypes.ANNOTATION_DEF,
189            TokenTypes.ANNOTATION_FIELD_DEF,
190            TokenTypes.ENUM_DEF,
191            TokenTypes.ENUM_CONSTANT_DEF,
192            TokenTypes.CLASS_DEF,
193            TokenTypes.INTERFACE_DEF,
194            TokenTypes.METHOD_DEF,
195            TokenTypes.CTOR_DEF,
196            TokenTypes.VARIABLE_DEF,
197            TokenTypes.RECORD_DEF,
198            TokenTypes.COMPACT_CTOR_DEF,
199        };
200    }
201
202    @Override
203    public int[] getAcceptableTokens() {
204        return getRequiredTokens();
205    }
206
207    @Override
208    public void visitToken(DetailAST ast) {
209        if (ast.getType() == TokenTypes.IDENT) {
210            if (collect) {
211                processIdent(ast);
212            }
213        }
214        else if (ast.getType() == TokenTypes.IMPORT) {
215            processImport(ast);
216        }
217        else if (ast.getType() == TokenTypes.STATIC_IMPORT) {
218            processStaticImport(ast);
219        }
220        else {
221            collect = true;
222            if (processJavadoc) {
223                collectReferencesFromJavadoc(ast);
224            }
225        }
226    }
227
228    /**
229     * Checks whether an import is unused.
230     *
231     * @param imprt an import.
232     * @return true if an import is unused.
233     */
234    private boolean isUnusedImport(String imprt) {
235        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
236        return !referenced.contains(CommonUtil.baseClassName(imprt))
237            || javaLangPackageMatcher.matches();
238    }
239
240    /**
241     * Collects references made by IDENT.
242     *
243     * @param ast the IDENT node to process
244     */
245    private void processIdent(DetailAST ast) {
246        final DetailAST parent = ast.getParent();
247        final int parentType = parent.getType();
248        if (parentType != TokenTypes.DOT
249            && parentType != TokenTypes.METHOD_DEF
250            || parentType == TokenTypes.DOT
251                && ast.getNextSibling() != null) {
252            referenced.add(ast.getText());
253        }
254    }
255
256    /**
257     * Collects the details of imports.
258     *
259     * @param ast node containing the import details
260     */
261    private void processImport(DetailAST ast) {
262        final FullIdent name = FullIdent.createFullIdentBelow(ast);
263        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
264            imports.add(name);
265        }
266    }
267
268    /**
269     * Collects the details of static imports.
270     *
271     * @param ast node containing the static import details
272     */
273    private void processStaticImport(DetailAST ast) {
274        final FullIdent name =
275            FullIdent.createFullIdent(
276                ast.getFirstChild().getNextSibling());
277        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
278            imports.add(name);
279        }
280    }
281
282    /**
283     * Collects references made in Javadoc comments.
284     *
285     * @param ast node to inspect for Javadoc
286     */
287    private void collectReferencesFromJavadoc(DetailAST ast) {
288        final FileContents contents = getFileContents();
289        final int lineNo = ast.getLineNo();
290        final TextBlock textBlock = contents.getJavadocBefore(lineNo);
291        if (textBlock != null) {
292            referenced.addAll(collectReferencesFromJavadoc(textBlock));
293        }
294    }
295
296    /**
297     * Process a javadoc {@link TextBlock} and return the set of classes
298     * referenced within.
299     *
300     * @param textBlock The javadoc block to parse
301     * @return a set of classes referenced in the javadoc block
302     */
303    private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
304        final List<JavadocTag> tags = new ArrayList<>();
305        // gather all the inline tags, like @link
306        // INLINE tags inside BLOCKs get hidden when using ALL
307        tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE));
308        // gather all the block-level tags, like @throws and @see
309        tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK));
310
311        final Set<String> references = new HashSet<>();
312
313        tags.stream()
314            .filter(JavadocTag::canReferenceImports)
315            .forEach(tag -> references.addAll(processJavadocTag(tag)));
316        return references;
317    }
318
319    /**
320     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
321     *
322     * @param cmt The javadoc block to parse
323     * @param tagType The type of tags we're interested in
324     * @return the list of tags
325     */
326    private static List<JavadocTag> getValidTags(TextBlock cmt,
327            JavadocUtil.JavadocTagType tagType) {
328        return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags();
329    }
330
331    /**
332     * Returns a list of references found in a javadoc {@link JavadocTag}.
333     *
334     * @param tag The javadoc tag to parse
335     * @return A list of references found in this tag
336     */
337    private static Set<String> processJavadocTag(JavadocTag tag) {
338        final Set<String> references = new HashSet<>();
339        final String identifier = tag.getFirstArg().trim();
340        for (Pattern pattern : new Pattern[]
341        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
342            references.addAll(matchPattern(identifier, pattern));
343        }
344        return references;
345    }
346
347    /**
348     * Extracts a list of texts matching a {@link Pattern} from a
349     * {@link String}.
350     *
351     * @param identifier The String to match the pattern against
352     * @param pattern The Pattern used to extract the texts
353     * @return A list of texts which matched the pattern
354     */
355    private static Set<String> matchPattern(String identifier, Pattern pattern) {
356        final Set<String> references = new HashSet<>();
357        final Matcher matcher = pattern.matcher(identifier);
358        while (matcher.find()) {
359            references.add(topLevelType(matcher.group(1)));
360        }
361        return references;
362    }
363
364    /**
365     * If the given type string contains "." (e.g. "Map.Entry"), returns the
366     * top level type (e.g. "Map"), as that is what must be imported for the
367     * type to resolve. Otherwise, returns the type as-is.
368     *
369     * @param type A possibly qualified type name
370     * @return The simple name of the top level type
371     */
372    private static String topLevelType(String type) {
373        final String topLevelType;
374        final int dotIndex = type.indexOf('.');
375        if (dotIndex == -1) {
376            topLevelType = type;
377        }
378        else {
379            topLevelType = type.substring(0, dotIndex);
380        }
381        return topLevelType;
382    }
383
384}