001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 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.HashSet;
023import java.util.List;
024import java.util.Set;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import com.google.common.collect.Sets;
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 = Pattern.compile(
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 = Pattern.compile(
066           "^" + CLASS_NAME);
067    /** Regex to match argument names. */
068    private static final Pattern ARGUMENT_NAME = Pattern.compile(
069           "[(,]\\s*" + CLASS_NAME.pattern());
070
071    /** Suffix for the star import. */
072    private static final String STAR_IMPORT_SUFFIX = ".*";
073
074    /** Set of the imports. */
075    private final Set<FullIdent> imports = Sets.newHashSet();
076
077    /** Set of references - possibly to imports or other things. */
078    private final Set<String> referenced = Sets.newHashSet();
079
080    /** Flag to indicate when time to start collecting references. */
081    private boolean collect;
082    /** Flag whether to process Javadoc comments. */
083    private boolean processJavadoc = true;
084
085    /**
086     * Sets whether to process JavaDoc or not.
087     *
088     * @param value Flag for processing JavaDoc.
089     */
090    public void setProcessJavadoc(boolean value) {
091        processJavadoc = value;
092    }
093
094    @Override
095    public void beginTree(DetailAST rootAST) {
096        collect = false;
097        imports.clear();
098        referenced.clear();
099    }
100
101    @Override
102    public void finishTree(DetailAST rootAST) {
103        // loop over all the imports to see if referenced.
104        for (final FullIdent imp : imports) {
105            if (!referenced.contains(CommonUtils.baseClassName(imp.getText()))) {
106                log(imp.getLineNo(),
107                    imp.getColumnNo(),
108                    MSG_KEY, imp.getText());
109            }
110        }
111    }
112
113    @Override
114    public int[] getDefaultTokens() {
115        return new int[] {
116            TokenTypes.IDENT,
117            TokenTypes.IMPORT,
118            TokenTypes.STATIC_IMPORT,
119            // Definitions that may contain Javadoc...
120            TokenTypes.PACKAGE_DEF,
121            TokenTypes.ANNOTATION_DEF,
122            TokenTypes.ANNOTATION_FIELD_DEF,
123            TokenTypes.ENUM_DEF,
124            TokenTypes.ENUM_CONSTANT_DEF,
125            TokenTypes.CLASS_DEF,
126            TokenTypes.INTERFACE_DEF,
127            TokenTypes.METHOD_DEF,
128            TokenTypes.CTOR_DEF,
129            TokenTypes.VARIABLE_DEF,
130        };
131    }
132
133    @Override
134    public int[] getRequiredTokens() {
135        return getDefaultTokens();
136    }
137
138    @Override
139    public int[] getAcceptableTokens() {
140        return new int[] {
141            TokenTypes.IDENT,
142            TokenTypes.IMPORT,
143            TokenTypes.STATIC_IMPORT,
144            // Definitions that may contain Javadoc...
145            TokenTypes.PACKAGE_DEF,
146            TokenTypes.ANNOTATION_DEF,
147            TokenTypes.ANNOTATION_FIELD_DEF,
148            TokenTypes.ENUM_DEF,
149            TokenTypes.ENUM_CONSTANT_DEF,
150            TokenTypes.CLASS_DEF,
151            TokenTypes.INTERFACE_DEF,
152            TokenTypes.METHOD_DEF,
153            TokenTypes.CTOR_DEF,
154            TokenTypes.VARIABLE_DEF,
155        };
156    }
157
158    @Override
159    public void visitToken(DetailAST ast) {
160        if (ast.getType() == TokenTypes.IDENT) {
161            if (collect) {
162                processIdent(ast);
163            }
164        }
165        else if (ast.getType() == TokenTypes.IMPORT) {
166            processImport(ast);
167        }
168        else if (ast.getType() == TokenTypes.STATIC_IMPORT) {
169            processStaticImport(ast);
170        }
171        else {
172            collect = true;
173            if (processJavadoc) {
174                collectReferencesFromJavadoc(ast);
175            }
176        }
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 Set<String> references = new HashSet<>();
239        // process all the @link type tags
240        // INLINE tags inside BLOCKs get hidden when using ALL
241        for (final JavadocTag tag
242                : getValidTags(textBlock, JavadocUtils.JavadocTagType.INLINE)) {
243            if (tag.canReferenceImports()) {
244                references.addAll(processJavadocTag(tag));
245            }
246        }
247        // process all the @throws type tags
248        for (final JavadocTag tag
249                : getValidTags(textBlock, JavadocUtils.JavadocTagType.BLOCK)) {
250            if (tag.canReferenceImports()) {
251                references.addAll(
252                        matchPattern(tag.getFirstArg(), FIRST_CLASS_NAME));
253            }
254        }
255        return references;
256    }
257
258    /**
259     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
260     * @param cmt The javadoc block to parse
261     * @param tagType The type of tags we're interested in
262     * @return the list of tags
263     */
264    private static List<JavadocTag> getValidTags(TextBlock cmt,
265            JavadocUtils.JavadocTagType tagType) {
266        return JavadocUtils.getJavadocTags(cmt, tagType).getValidTags();
267    }
268
269    /**
270     * Returns a list of references found in a javadoc {@link JavadocTag}.
271     * @param tag The javadoc tag to parse
272     * @return A list of references found in this tag
273     */
274    private static Set<String> processJavadocTag(JavadocTag tag) {
275        final Set<String> references = new HashSet<>();
276        final String identifier = tag.getFirstArg().trim();
277        for (Pattern pattern : new Pattern[]
278        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
279            references.addAll(matchPattern(identifier, pattern));
280        }
281        return references;
282    }
283
284    /**
285     * Extracts a list of texts matching a {@link Pattern} from a
286     * {@link String}.
287     * @param identifier The String to match the pattern against
288     * @param pattern The Pattern used to extract the texts
289     * @return A list of texts which matched the pattern
290     */
291    private static Set<String> matchPattern(String identifier, Pattern pattern) {
292        final Set<String> references = new HashSet<>();
293        final Matcher matcher = pattern.matcher(identifier);
294        while (matcher.find()) {
295            references.add(matcher.group(1));
296        }
297        return references;
298    }
299}