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