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