001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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. Checkstyle uses a simple but very 043 * reliable algorithm to report on unused import statements. An import statement 044 * is considered unused if: 045 * </p> 046 * <ul> 047 * <li> 048 * It is not referenced in the file. The algorithm does not support wild-card 049 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated 050 * checks for imports that handle wild-card imports. 051 * </li> 052 * <li> 053 * It is a duplicate of another import. This is when a class is imported more 054 * than once. 055 * </li> 056 * <li> 057 * The class imported is from the {@code java.lang} package. For example 058 * importing {@code java.lang.String}. 059 * </li> 060 * <li> 061 * The class imported is from the same package. 062 * </li> 063 * <li> 064 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by 065 * default, but it is considered bad practice to introduce a compile time 066 * dependency for documentation purposes only. As an example, the import 067 * {@code java.util.List} would be considered referenced with the Javadoc 068 * comment {@code {@link List}}. The alternative to avoid introducing a compile 069 * time dependency would be to write the Javadoc comment as {@code {@link java.util.List}}. 070 * </li> 071 * </ul> 072 * <p> 073 * The main limitation of this check is handling the case where an imported type 074 * has the same name as a declaration, such as a member variable. 075 * </p> 076 * <p> 077 * For example, in the following case the import {@code java.awt.Component} 078 * will not be flagged as unused: 079 * </p> 080 * <pre> 081 * import java.awt.Component; 082 * class FooBar { 083 * private Object Component; // a bad practice in my opinion 084 * ... 085 * } 086 * </pre> 087 * <ul> 088 * <li> 089 * Property {@code processJavadoc} - Control whether to process Javadoc comments. 090 * Default value is {@code true}. 091 * </li> 092 * </ul> 093 * <p> 094 * To configure the check: 095 * </p> 096 * <pre> 097 * <module name="UnusedImports"/> 098 * </pre> 099 * 100 * @since 3.0 101 */ 102@FileStatefulCheck 103public class UnusedImportsCheck extends AbstractCheck { 104 105 /** 106 * A key is pointing to the warning message text in "messages.properties" 107 * file. 108 */ 109 public static final String MSG_KEY = "import.unused"; 110 111 /** Regex to match class names. */ 112 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 113 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 114 /** Regex to match the first class name. */ 115 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 116 "^" + CLASS_NAME); 117 /** Regex to match argument names. */ 118 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 119 "[(,]\\s*" + CLASS_NAME.pattern()); 120 121 /** Regexp pattern to match java.lang package. */ 122 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 123 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 124 125 /** Suffix for the star import. */ 126 private static final String STAR_IMPORT_SUFFIX = ".*"; 127 128 /** Set of the imports. */ 129 private final Set<FullIdent> imports = new HashSet<>(); 130 131 /** Set of references - possibly to imports or other things. */ 132 private final Set<String> referenced = new HashSet<>(); 133 134 /** Flag to indicate when time to start collecting references. */ 135 private boolean collect; 136 /** Control whether to process Javadoc comments. */ 137 private boolean processJavadoc = true; 138 139 /** 140 * Setter to control whether to process Javadoc comments. 141 * 142 * @param value Flag for processing Javadoc comments. 143 */ 144 public void setProcessJavadoc(boolean value) { 145 processJavadoc = value; 146 } 147 148 @Override 149 public void beginTree(DetailAST rootAST) { 150 collect = false; 151 imports.clear(); 152 referenced.clear(); 153 } 154 155 @Override 156 public void finishTree(DetailAST rootAST) { 157 // loop over all the imports to see if referenced. 158 imports.stream() 159 .filter(imprt -> isUnusedImport(imprt.getText())) 160 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 161 } 162 163 @Override 164 public int[] getDefaultTokens() { 165 return getRequiredTokens(); 166 } 167 168 @Override 169 public int[] getRequiredTokens() { 170 return new int[] { 171 TokenTypes.IDENT, 172 TokenTypes.IMPORT, 173 TokenTypes.STATIC_IMPORT, 174 // Definitions that may contain Javadoc... 175 TokenTypes.PACKAGE_DEF, 176 TokenTypes.ANNOTATION_DEF, 177 TokenTypes.ANNOTATION_FIELD_DEF, 178 TokenTypes.ENUM_DEF, 179 TokenTypes.ENUM_CONSTANT_DEF, 180 TokenTypes.CLASS_DEF, 181 TokenTypes.INTERFACE_DEF, 182 TokenTypes.METHOD_DEF, 183 TokenTypes.CTOR_DEF, 184 TokenTypes.VARIABLE_DEF, 185 }; 186 } 187 188 @Override 189 public int[] getAcceptableTokens() { 190 return getRequiredTokens(); 191 } 192 193 @Override 194 public void visitToken(DetailAST ast) { 195 if (ast.getType() == TokenTypes.IDENT) { 196 if (collect) { 197 processIdent(ast); 198 } 199 } 200 else if (ast.getType() == TokenTypes.IMPORT) { 201 processImport(ast); 202 } 203 else if (ast.getType() == TokenTypes.STATIC_IMPORT) { 204 processStaticImport(ast); 205 } 206 else { 207 collect = true; 208 if (processJavadoc) { 209 collectReferencesFromJavadoc(ast); 210 } 211 } 212 } 213 214 /** 215 * Checks whether an import is unused. 216 * 217 * @param imprt an import. 218 * @return true if an import is unused. 219 */ 220 private boolean isUnusedImport(String imprt) { 221 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 222 return !referenced.contains(CommonUtil.baseClassName(imprt)) 223 || javaLangPackageMatcher.matches(); 224 } 225 226 /** 227 * Collects references made by IDENT. 228 * 229 * @param ast the IDENT node to process 230 */ 231 private void processIdent(DetailAST ast) { 232 final DetailAST parent = ast.getParent(); 233 final int parentType = parent.getType(); 234 if (parentType != TokenTypes.DOT 235 && parentType != TokenTypes.METHOD_DEF 236 || parentType == TokenTypes.DOT 237 && ast.getNextSibling() != null) { 238 referenced.add(ast.getText()); 239 } 240 } 241 242 /** 243 * Collects the details of imports. 244 * 245 * @param ast node containing the import details 246 */ 247 private void processImport(DetailAST ast) { 248 final FullIdent name = FullIdent.createFullIdentBelow(ast); 249 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 250 imports.add(name); 251 } 252 } 253 254 /** 255 * Collects the details of static imports. 256 * 257 * @param ast node containing the static import details 258 */ 259 private void processStaticImport(DetailAST ast) { 260 final FullIdent name = 261 FullIdent.createFullIdent( 262 ast.getFirstChild().getNextSibling()); 263 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 264 imports.add(name); 265 } 266 } 267 268 /** 269 * Collects references made in Javadoc comments. 270 * 271 * @param ast node to inspect for Javadoc 272 */ 273 private void collectReferencesFromJavadoc(DetailAST ast) { 274 final FileContents contents = getFileContents(); 275 final int lineNo = ast.getLineNo(); 276 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 277 if (textBlock != null) { 278 referenced.addAll(collectReferencesFromJavadoc(textBlock)); 279 } 280 } 281 282 /** 283 * Process a javadoc {@link TextBlock} and return the set of classes 284 * referenced within. 285 * 286 * @param textBlock The javadoc block to parse 287 * @return a set of classes referenced in the javadoc block 288 */ 289 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 290 final List<JavadocTag> tags = new ArrayList<>(); 291 // gather all the inline tags, like @link 292 // INLINE tags inside BLOCKs get hidden when using ALL 293 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE)); 294 // gather all the block-level tags, like @throws and @see 295 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK)); 296 297 final Set<String> references = new HashSet<>(); 298 299 tags.stream() 300 .filter(JavadocTag::canReferenceImports) 301 .forEach(tag -> references.addAll(processJavadocTag(tag))); 302 return references; 303 } 304 305 /** 306 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 307 * 308 * @param cmt The javadoc block to parse 309 * @param tagType The type of tags we're interested in 310 * @return the list of tags 311 */ 312 private static List<JavadocTag> getValidTags(TextBlock cmt, 313 JavadocUtil.JavadocTagType tagType) { 314 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags(); 315 } 316 317 /** 318 * Returns a list of references found in a javadoc {@link JavadocTag}. 319 * 320 * @param tag The javadoc tag to parse 321 * @return A list of references found in this tag 322 */ 323 private static Set<String> processJavadocTag(JavadocTag tag) { 324 final Set<String> references = new HashSet<>(); 325 final String identifier = tag.getFirstArg().trim(); 326 for (Pattern pattern : new Pattern[] 327 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 328 references.addAll(matchPattern(identifier, pattern)); 329 } 330 return references; 331 } 332 333 /** 334 * Extracts a list of texts matching a {@link Pattern} from a 335 * {@link String}. 336 * 337 * @param identifier The String to match the pattern against 338 * @param pattern The Pattern used to extract the texts 339 * @return A list of texts which matched the pattern 340 */ 341 private static Set<String> matchPattern(String identifier, Pattern pattern) { 342 final Set<String> references = new HashSet<>(); 343 final Matcher matcher = pattern.matcher(identifier); 344 while (matcher.find()) { 345 references.add(topLevelType(matcher.group(1))); 346 } 347 return references; 348 } 349 350 /** 351 * If the given type string contains "." (e.g. "Map.Entry"), returns the 352 * top level type (e.g. "Map"), as that is what must be imported for the 353 * type to resolve. Otherwise, returns the type as-is. 354 * 355 * @param type A possibly qualified type name 356 * @return The simple name of the top level type 357 */ 358 private static String topLevelType(String type) { 359 final String topLevelType; 360 final int dotIndex = type.indexOf('.'); 361 if (dotIndex == -1) { 362 topLevelType = type; 363 } 364 else { 365 topLevelType = type.substring(0, dotIndex); 366 } 367 return topLevelType; 368 } 369 370}