001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.CommonUtils;
038import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
039
040/**
041 * <p>
042 * Checks for unused import statements.
043 * </p>
044 *  <p>
045 * An example of how to configure the check is:
046 * </p>
047 * <pre>
048 * &lt;module name="UnusedImports"/&gt;
049 * </pre>
050 * Compatible with Java 1.5 source.
051 *
052 * @author Oliver Burn
053 */
054@FileStatefulCheck
055public class UnusedImportsCheck extends AbstractCheck {
056
057    /**
058     * A key is pointing to the warning message text in "messages.properties"
059     * file.
060     */
061    public static final String MSG_KEY = "import.unused";
062
063    /** Regex to match class names. */
064    private static final Pattern CLASS_NAME = CommonUtils.createPattern(
065           "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
066    /** Regex to match the first class name. */
067    private static final Pattern FIRST_CLASS_NAME = CommonUtils.createPattern(
068           "^" + CLASS_NAME);
069    /** Regex to match argument names. */
070    private static final Pattern ARGUMENT_NAME = CommonUtils.createPattern(
071           "[(,]\\s*" + CLASS_NAME.pattern());
072
073    /** Regexp pattern to match java.lang package. */
074    private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
075        CommonUtils.createPattern("^java\\.lang\\.[a-zA-Z]+$");
076
077    /** Suffix for the star import. */
078    private static final String STAR_IMPORT_SUFFIX = ".*";
079
080    /** Set of the imports. */
081    private final Set<FullIdent> imports = new HashSet<>();
082
083    /** Set of references - possibly to imports or other things. */
084    private final Set<String> referenced = new HashSet<>();
085
086    /** Flag to indicate when time to start collecting references. */
087    private boolean collect;
088    /** Flag whether to process Javadoc comments. */
089    private boolean processJavadoc = true;
090
091    /**
092     * Sets whether to process JavaDoc or not.
093     *
094     * @param value Flag for processing JavaDoc.
095     */
096    public void setProcessJavadoc(boolean value) {
097        processJavadoc = value;
098    }
099
100    @Override
101    public void beginTree(DetailAST rootAST) {
102        collect = false;
103        imports.clear();
104        referenced.clear();
105    }
106
107    @Override
108    public void finishTree(DetailAST rootAST) {
109        // loop over all the imports to see if referenced.
110        imports.stream()
111            .filter(imprt -> isUnusedImport(imprt.getText()))
112            .forEach(imprt -> log(imprt.getLineNo(),
113                imprt.getColumnNo(),
114                MSG_KEY, imprt.getText()));
115    }
116
117    @Override
118    public int[] getDefaultTokens() {
119        return getRequiredTokens();
120    }
121
122    @Override
123    public int[] getRequiredTokens() {
124        return new int[] {
125            TokenTypes.IDENT,
126            TokenTypes.IMPORT,
127            TokenTypes.STATIC_IMPORT,
128            // Definitions that may contain Javadoc...
129            TokenTypes.PACKAGE_DEF,
130            TokenTypes.ANNOTATION_DEF,
131            TokenTypes.ANNOTATION_FIELD_DEF,
132            TokenTypes.ENUM_DEF,
133            TokenTypes.ENUM_CONSTANT_DEF,
134            TokenTypes.CLASS_DEF,
135            TokenTypes.INTERFACE_DEF,
136            TokenTypes.METHOD_DEF,
137            TokenTypes.CTOR_DEF,
138            TokenTypes.VARIABLE_DEF,
139        };
140    }
141
142    @Override
143    public int[] getAcceptableTokens() {
144        return getRequiredTokens();
145    }
146
147    @Override
148    public void visitToken(DetailAST ast) {
149        if (ast.getType() == TokenTypes.IDENT) {
150            if (collect) {
151                processIdent(ast);
152            }
153        }
154        else if (ast.getType() == TokenTypes.IMPORT) {
155            processImport(ast);
156        }
157        else if (ast.getType() == TokenTypes.STATIC_IMPORT) {
158            processStaticImport(ast);
159        }
160        else {
161            collect = true;
162            if (processJavadoc) {
163                collectReferencesFromJavadoc(ast);
164            }
165        }
166    }
167
168    /**
169     * Checks whether an import is unused.
170     * @param imprt an import.
171     * @return true if an import is unused.
172     */
173    private boolean isUnusedImport(String imprt) {
174        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
175        return !referenced.contains(CommonUtils.baseClassName(imprt))
176            || javaLangPackageMatcher.matches();
177    }
178
179    /**
180     * Collects references made by IDENT.
181     * @param ast the IDENT node to process
182     */
183    private void processIdent(DetailAST ast) {
184        final DetailAST parent = ast.getParent();
185        final int parentType = parent.getType();
186        if (parentType != TokenTypes.DOT
187            && parentType != TokenTypes.METHOD_DEF
188            || parentType == TokenTypes.DOT
189                && ast.getNextSibling() != null) {
190            referenced.add(ast.getText());
191        }
192    }
193
194    /**
195     * Collects the details of imports.
196     * @param ast node containing the import details
197     */
198    private void processImport(DetailAST ast) {
199        final FullIdent name = FullIdent.createFullIdentBelow(ast);
200        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
201            imports.add(name);
202        }
203    }
204
205    /**
206     * Collects the details of static imports.
207     * @param ast node containing the static import details
208     */
209    private void processStaticImport(DetailAST ast) {
210        final FullIdent name =
211            FullIdent.createFullIdent(
212                ast.getFirstChild().getNextSibling());
213        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
214            imports.add(name);
215        }
216    }
217
218    /**
219     * Collects references made in Javadoc comments.
220     * @param ast node to inspect for Javadoc
221     */
222    private void collectReferencesFromJavadoc(DetailAST ast) {
223        final FileContents contents = getFileContents();
224        final int lineNo = ast.getLineNo();
225        final TextBlock textBlock = contents.getJavadocBefore(lineNo);
226        if (textBlock != null) {
227            referenced.addAll(collectReferencesFromJavadoc(textBlock));
228        }
229    }
230
231    /**
232     * Process a javadoc {@link TextBlock} and return the set of classes
233     * referenced within.
234     * @param textBlock The javadoc block to parse
235     * @return a set of classes referenced in the javadoc block
236     */
237    private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
238        final List<JavadocTag> tags = new ArrayList<>();
239        // gather all the inline tags, like @link
240        // INLINE tags inside BLOCKs get hidden when using ALL
241        tags.addAll(getValidTags(textBlock, JavadocUtils.JavadocTagType.INLINE));
242        // gather all the block-level tags, like @throws and @see
243        tags.addAll(getValidTags(textBlock, JavadocUtils.JavadocTagType.BLOCK));
244
245        final Set<String> references = new HashSet<>();
246
247        tags.stream()
248            .filter(JavadocTag::canReferenceImports)
249            .forEach(tag -> references.addAll(processJavadocTag(tag)));
250        return references;
251    }
252
253    /**
254     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
255     * @param cmt The javadoc block to parse
256     * @param tagType The type of tags we're interested in
257     * @return the list of tags
258     */
259    private static List<JavadocTag> getValidTags(TextBlock cmt,
260            JavadocUtils.JavadocTagType tagType) {
261        return JavadocUtils.getJavadocTags(cmt, tagType).getValidTags();
262    }
263
264    /**
265     * Returns a list of references found in a javadoc {@link JavadocTag}.
266     * @param tag The javadoc tag to parse
267     * @return A list of references found in this tag
268     */
269    private static Set<String> processJavadocTag(JavadocTag tag) {
270        final Set<String> references = new HashSet<>();
271        final String identifier = tag.getFirstArg().trim();
272        for (Pattern pattern : new Pattern[]
273        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
274            references.addAll(matchPattern(identifier, pattern));
275        }
276        return references;
277    }
278
279    /**
280     * Extracts a list of texts matching a {@link Pattern} from a
281     * {@link String}.
282     * @param identifier The String to match the pattern against
283     * @param pattern The Pattern used to extract the texts
284     * @return A list of texts which matched the pattern
285     */
286    private static Set<String> matchPattern(String identifier, Pattern pattern) {
287        final Set<String> references = new HashSet<>();
288        final Matcher matcher = pattern.matcher(identifier);
289        while (matcher.find()) {
290            references.add(topLevelType(matcher.group(1)));
291        }
292        return references;
293    }
294
295    /**
296     * If the given type string contains "." (e.g. "Map.Entry"), returns the
297     * top level type (e.g. "Map"), as that is what must be imported for the
298     * type to resolve. Otherwise, returns the type as-is.
299     * @param type A possibly qualified type name
300     * @return The simple name of the top level type
301     */
302    private static String topLevelType(String type) {
303        final String topLevelType;
304        final int dotIndex = type.indexOf('.');
305        if (dotIndex == -1) {
306            topLevelType = type;
307        }
308        else {
309            topLevelType = type.substring(0, dotIndex);
310        }
311        return topLevelType;
312    }
313
314}