1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.imports;
21  
22  import java.util.ArrayList;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.FileContents;
33  import com.puppycrawl.tools.checkstyle.api.FullIdent;
34  import com.puppycrawl.tools.checkstyle.api.TextBlock;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39  
40  /**
41   * <p>
42   * Checks for unused import statements.
43   * </p>
44   *  <p>
45   * An example of how to configure the check is:
46   * </p>
47   * <pre>
48   * &lt;module name="UnusedImports"/&gt;
49   * </pre>
50   * Compatible with Java 1.5 source.
51   *
52   */
53  @FileStatefulCheck
54  public class UnusedImportsCheck extends AbstractCheck {
55  
56      /**
57       * A key is pointing to the warning message text in "messages.properties"
58       * file.
59       */
60      public static final String MSG_KEY = "import.unused";
61  
62      /** Regex to match class names. */
63      private static final Pattern CLASS_NAME = CommonUtil.createPattern(
64             "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
65      /** Regex to match the first class name. */
66      private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
67             "^" + CLASS_NAME);
68      /** Regex to match argument names. */
69      private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
70             "[(,]\\s*" + CLASS_NAME.pattern());
71  
72      /** Regexp pattern to match java.lang package. */
73      private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
74          CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
75  
76      /** Suffix for the star import. */
77      private static final String STAR_IMPORT_SUFFIX = ".*";
78  
79      /** Set of the imports. */
80      private final Set<FullIdent> imports = new HashSet<>();
81  
82      /** Set of references - possibly to imports or other things. */
83      private final Set<String> referenced = new HashSet<>();
84  
85      /** Flag to indicate when time to start collecting references. */
86      private boolean collect;
87      /** Flag whether to process Javadoc comments. */
88      private boolean processJavadoc = true;
89  
90      /**
91       * Sets whether to process JavaDoc or not.
92       *
93       * @param value Flag for processing JavaDoc.
94       */
95      public void setProcessJavadoc(boolean value) {
96          processJavadoc = value;
97      }
98  
99      @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.getDetailAst(),
112                 MSG_KEY, imprt.getText()));
113     }
114 
115     @Override
116     public int[] getDefaultTokens() {
117         return getRequiredTokens();
118     }
119 
120     @Override
121     public int[] getRequiredTokens() {
122         return new int[] {
123             TokenTypes.IDENT,
124             TokenTypes.IMPORT,
125             TokenTypes.STATIC_IMPORT,
126             // Definitions that may contain Javadoc...
127             TokenTypes.PACKAGE_DEF,
128             TokenTypes.ANNOTATION_DEF,
129             TokenTypes.ANNOTATION_FIELD_DEF,
130             TokenTypes.ENUM_DEF,
131             TokenTypes.ENUM_CONSTANT_DEF,
132             TokenTypes.CLASS_DEF,
133             TokenTypes.INTERFACE_DEF,
134             TokenTypes.METHOD_DEF,
135             TokenTypes.CTOR_DEF,
136             TokenTypes.VARIABLE_DEF,
137         };
138     }
139 
140     @Override
141     public int[] getAcceptableTokens() {
142         return getRequiredTokens();
143     }
144 
145     @Override
146     public void visitToken(DetailAST ast) {
147         if (ast.getType() == TokenTypes.IDENT) {
148             if (collect) {
149                 processIdent(ast);
150             }
151         }
152         else if (ast.getType() == TokenTypes.IMPORT) {
153             processImport(ast);
154         }
155         else if (ast.getType() == TokenTypes.STATIC_IMPORT) {
156             processStaticImport(ast);
157         }
158         else {
159             collect = true;
160             if (processJavadoc) {
161                 collectReferencesFromJavadoc(ast);
162             }
163         }
164     }
165 
166     /**
167      * Checks whether an import is unused.
168      * @param imprt an import.
169      * @return true if an import is unused.
170      */
171     private boolean isUnusedImport(String imprt) {
172         final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
173         return !referenced.contains(CommonUtil.baseClassName(imprt))
174             || javaLangPackageMatcher.matches();
175     }
176 
177     /**
178      * Collects references made by IDENT.
179      * @param ast the IDENT node to process
180      */
181     private void processIdent(DetailAST ast) {
182         final DetailAST parent = ast.getParent();
183         final int parentType = parent.getType();
184         if (parentType != TokenTypes.DOT
185             && parentType != TokenTypes.METHOD_DEF
186             || parentType == TokenTypes.DOT
187                 && ast.getNextSibling() != null) {
188             referenced.add(ast.getText());
189         }
190     }
191 
192     /**
193      * Collects the details of imports.
194      * @param ast node containing the import details
195      */
196     private void processImport(DetailAST ast) {
197         final FullIdent name = FullIdent.createFullIdentBelow(ast);
198         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
199             imports.add(name);
200         }
201     }
202 
203     /**
204      * Collects the details of static imports.
205      * @param ast node containing the static import details
206      */
207     private void processStaticImport(DetailAST ast) {
208         final FullIdent name =
209             FullIdent.createFullIdent(
210                 ast.getFirstChild().getNextSibling());
211         if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
212             imports.add(name);
213         }
214     }
215 
216     /**
217      * Collects references made in Javadoc comments.
218      * @param ast node to inspect for Javadoc
219      */
220     private void collectReferencesFromJavadoc(DetailAST ast) {
221         final FileContents contents = getFileContents();
222         final int lineNo = ast.getLineNo();
223         final TextBlock textBlock = contents.getJavadocBefore(lineNo);
224         if (textBlock != null) {
225             referenced.addAll(collectReferencesFromJavadoc(textBlock));
226         }
227     }
228 
229     /**
230      * Process a javadoc {@link TextBlock} and return the set of classes
231      * referenced within.
232      * @param textBlock The javadoc block to parse
233      * @return a set of classes referenced in the javadoc block
234      */
235     private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
236         final List<JavadocTag> tags = new ArrayList<>();
237         // gather all the inline tags, like @link
238         // INLINE tags inside BLOCKs get hidden when using ALL
239         tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE));
240         // gather all the block-level tags, like @throws and @see
241         tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK));
242 
243         final Set<String> references = new HashSet<>();
244 
245         tags.stream()
246             .filter(JavadocTag::canReferenceImports)
247             .forEach(tag -> references.addAll(processJavadocTag(tag)));
248         return references;
249     }
250 
251     /**
252      * Returns the list of valid tags found in a javadoc {@link TextBlock}.
253      * @param cmt The javadoc block to parse
254      * @param tagType The type of tags we're interested in
255      * @return the list of tags
256      */
257     private static List<JavadocTag> getValidTags(TextBlock cmt,
258             JavadocUtil.JavadocTagType tagType) {
259         return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags();
260     }
261 
262     /**
263      * Returns a list of references found in a javadoc {@link JavadocTag}.
264      * @param tag The javadoc tag to parse
265      * @return A list of references found in this tag
266      */
267     private static Set<String> processJavadocTag(JavadocTag tag) {
268         final Set<String> references = new HashSet<>();
269         final String identifier = tag.getFirstArg().trim();
270         for (Pattern pattern : new Pattern[]
271         {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
272             references.addAll(matchPattern(identifier, pattern));
273         }
274         return references;
275     }
276 
277     /**
278      * Extracts a list of texts matching a {@link Pattern} from a
279      * {@link String}.
280      * @param identifier The String to match the pattern against
281      * @param pattern The Pattern used to extract the texts
282      * @return A list of texts which matched the pattern
283      */
284     private static Set<String> matchPattern(String identifier, Pattern pattern) {
285         final Set<String> references = new HashSet<>();
286         final Matcher matcher = pattern.matcher(identifier);
287         while (matcher.find()) {
288             references.add(topLevelType(matcher.group(1)));
289         }
290         return references;
291     }
292 
293     /**
294      * If the given type string contains "." (e.g. "Map.Entry"), returns the
295      * top level type (e.g. "Map"), as that is what must be imported for the
296      * type to resolve. Otherwise, returns the type as-is.
297      * @param type A possibly qualified type name
298      * @return The simple name of the top level type
299      */
300     private static String topLevelType(String type) {
301         final String topLevelType;
302         final int dotIndex = type.indexOf('.');
303         if (dotIndex == -1) {
304             topLevelType = type;
305         }
306         else {
307             topLevelType = type.substring(0, dotIndex);
308         }
309         return topLevelType;
310     }
311 
312 }