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 * Type is {@code boolean}. 091 * Default value is {@code true}. 092 * </li> 093 * </ul> 094 * <p> 095 * To configure the check: 096 * </p> 097 * <pre> 098 * <module name="UnusedImports"/> 099 * </pre> 100 * <p> 101 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 102 * </p> 103 * <p> 104 * Violation Message Keys: 105 * </p> 106 * <ul> 107 * <li> 108 * {@code import.unused} 109 * </li> 110 * </ul> 111 * 112 * @since 3.0 113 */ 114@FileStatefulCheck 115public class UnusedImportsCheck extends AbstractCheck { 116 117 /** 118 * A key is pointing to the warning message text in "messages.properties" 119 * file. 120 */ 121 public static final String MSG_KEY = "import.unused"; 122 123 /** Regex to match class names. */ 124 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 125 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 126 /** Regex to match the first class name. */ 127 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 128 "^" + CLASS_NAME); 129 /** Regex to match argument names. */ 130 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 131 "[(,]\\s*" + CLASS_NAME.pattern()); 132 133 /** Regexp pattern to match java.lang package. */ 134 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 135 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 136 137 /** Suffix for the star import. */ 138 private static final String STAR_IMPORT_SUFFIX = ".*"; 139 140 /** Set of the imports. */ 141 private final Set<FullIdent> imports = new HashSet<>(); 142 143 /** Set of references - possibly to imports or other things. */ 144 private final Set<String> referenced = new HashSet<>(); 145 146 /** Flag to indicate when time to start collecting references. */ 147 private boolean collect; 148 /** Control whether to process Javadoc comments. */ 149 private boolean processJavadoc = true; 150 151 /** 152 * Setter to control whether to process Javadoc comments. 153 * 154 * @param value Flag for processing Javadoc comments. 155 */ 156 public void setProcessJavadoc(boolean value) { 157 processJavadoc = value; 158 } 159 160 @Override 161 public void beginTree(DetailAST rootAST) { 162 collect = false; 163 imports.clear(); 164 referenced.clear(); 165 } 166 167 @Override 168 public void finishTree(DetailAST rootAST) { 169 // loop over all the imports to see if referenced. 170 imports.stream() 171 .filter(imprt -> isUnusedImport(imprt.getText())) 172 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 173 } 174 175 @Override 176 public int[] getDefaultTokens() { 177 return getRequiredTokens(); 178 } 179 180 @Override 181 public int[] getRequiredTokens() { 182 return new int[] { 183 TokenTypes.IDENT, 184 TokenTypes.IMPORT, 185 TokenTypes.STATIC_IMPORT, 186 // Definitions that may contain Javadoc... 187 TokenTypes.PACKAGE_DEF, 188 TokenTypes.ANNOTATION_DEF, 189 TokenTypes.ANNOTATION_FIELD_DEF, 190 TokenTypes.ENUM_DEF, 191 TokenTypes.ENUM_CONSTANT_DEF, 192 TokenTypes.CLASS_DEF, 193 TokenTypes.INTERFACE_DEF, 194 TokenTypes.METHOD_DEF, 195 TokenTypes.CTOR_DEF, 196 TokenTypes.VARIABLE_DEF, 197 }; 198 } 199 200 @Override 201 public int[] getAcceptableTokens() { 202 return getRequiredTokens(); 203 } 204 205 @Override 206 public void visitToken(DetailAST ast) { 207 if (ast.getType() == TokenTypes.IDENT) { 208 if (collect) { 209 processIdent(ast); 210 } 211 } 212 else if (ast.getType() == TokenTypes.IMPORT) { 213 processImport(ast); 214 } 215 else if (ast.getType() == TokenTypes.STATIC_IMPORT) { 216 processStaticImport(ast); 217 } 218 else { 219 collect = true; 220 if (processJavadoc) { 221 collectReferencesFromJavadoc(ast); 222 } 223 } 224 } 225 226 /** 227 * Checks whether an import is unused. 228 * 229 * @param imprt an import. 230 * @return true if an import is unused. 231 */ 232 private boolean isUnusedImport(String imprt) { 233 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 234 return !referenced.contains(CommonUtil.baseClassName(imprt)) 235 || javaLangPackageMatcher.matches(); 236 } 237 238 /** 239 * Collects references made by IDENT. 240 * 241 * @param ast the IDENT node to process 242 */ 243 private void processIdent(DetailAST ast) { 244 final DetailAST parent = ast.getParent(); 245 final int parentType = parent.getType(); 246 if (parentType != TokenTypes.DOT 247 && parentType != TokenTypes.METHOD_DEF 248 || parentType == TokenTypes.DOT 249 && ast.getNextSibling() != null) { 250 referenced.add(ast.getText()); 251 } 252 } 253 254 /** 255 * Collects the details of imports. 256 * 257 * @param ast node containing the import details 258 */ 259 private void processImport(DetailAST ast) { 260 final FullIdent name = FullIdent.createFullIdentBelow(ast); 261 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 262 imports.add(name); 263 } 264 } 265 266 /** 267 * Collects the details of static imports. 268 * 269 * @param ast node containing the static import details 270 */ 271 private void processStaticImport(DetailAST ast) { 272 final FullIdent name = 273 FullIdent.createFullIdent( 274 ast.getFirstChild().getNextSibling()); 275 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 276 imports.add(name); 277 } 278 } 279 280 /** 281 * Collects references made in Javadoc comments. 282 * 283 * @param ast node to inspect for Javadoc 284 */ 285 private void collectReferencesFromJavadoc(DetailAST ast) { 286 final FileContents contents = getFileContents(); 287 final int lineNo = ast.getLineNo(); 288 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 289 if (textBlock != null) { 290 referenced.addAll(collectReferencesFromJavadoc(textBlock)); 291 } 292 } 293 294 /** 295 * Process a javadoc {@link TextBlock} and return the set of classes 296 * referenced within. 297 * 298 * @param textBlock The javadoc block to parse 299 * @return a set of classes referenced in the javadoc block 300 */ 301 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 302 final List<JavadocTag> tags = new ArrayList<>(); 303 // gather all the inline tags, like @link 304 // INLINE tags inside BLOCKs get hidden when using ALL 305 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE)); 306 // gather all the block-level tags, like @throws and @see 307 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK)); 308 309 final Set<String> references = new HashSet<>(); 310 311 tags.stream() 312 .filter(JavadocTag::canReferenceImports) 313 .forEach(tag -> references.addAll(processJavadocTag(tag))); 314 return references; 315 } 316 317 /** 318 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 319 * 320 * @param cmt The javadoc block to parse 321 * @param tagType The type of tags we're interested in 322 * @return the list of tags 323 */ 324 private static List<JavadocTag> getValidTags(TextBlock cmt, 325 JavadocUtil.JavadocTagType tagType) { 326 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags(); 327 } 328 329 /** 330 * Returns a list of references found in a javadoc {@link JavadocTag}. 331 * 332 * @param tag The javadoc tag to parse 333 * @return A list of references found in this tag 334 */ 335 private static Set<String> processJavadocTag(JavadocTag tag) { 336 final Set<String> references = new HashSet<>(); 337 final String identifier = tag.getFirstArg().trim(); 338 for (Pattern pattern : new Pattern[] 339 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 340 references.addAll(matchPattern(identifier, pattern)); 341 } 342 return references; 343 } 344 345 /** 346 * Extracts a list of texts matching a {@link Pattern} from a 347 * {@link String}. 348 * 349 * @param identifier The String to match the pattern against 350 * @param pattern The Pattern used to extract the texts 351 * @return A list of texts which matched the pattern 352 */ 353 private static Set<String> matchPattern(String identifier, Pattern pattern) { 354 final Set<String> references = new HashSet<>(); 355 final Matcher matcher = pattern.matcher(identifier); 356 while (matcher.find()) { 357 references.add(topLevelType(matcher.group(1))); 358 } 359 return references; 360 } 361 362 /** 363 * If the given type string contains "." (e.g. "Map.Entry"), returns the 364 * top level type (e.g. "Map"), as that is what must be imported for the 365 * type to resolve. Otherwise, returns the type as-is. 366 * 367 * @param type A possibly qualified type name 368 * @return The simple name of the top level type 369 */ 370 private static String topLevelType(String type) { 371 final String topLevelType; 372 final int dotIndex = type.indexOf('.'); 373 if (dotIndex == -1) { 374 topLevelType = type; 375 } 376 else { 377 topLevelType = type.substring(0, dotIndex); 378 } 379 return topLevelType; 380 } 381 382}