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 TokenTypes.RECORD_DEF, 198 TokenTypes.COMPACT_CTOR_DEF, 199 }; 200 } 201 202 @Override 203 public int[] getAcceptableTokens() { 204 return getRequiredTokens(); 205 } 206 207 @Override 208 public void visitToken(DetailAST ast) { 209 if (ast.getType() == TokenTypes.IDENT) { 210 if (collect) { 211 processIdent(ast); 212 } 213 } 214 else if (ast.getType() == TokenTypes.IMPORT) { 215 processImport(ast); 216 } 217 else if (ast.getType() == TokenTypes.STATIC_IMPORT) { 218 processStaticImport(ast); 219 } 220 else { 221 collect = true; 222 if (processJavadoc) { 223 collectReferencesFromJavadoc(ast); 224 } 225 } 226 } 227 228 /** 229 * Checks whether an import is unused. 230 * 231 * @param imprt an import. 232 * @return true if an import is unused. 233 */ 234 private boolean isUnusedImport(String imprt) { 235 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 236 return !referenced.contains(CommonUtil.baseClassName(imprt)) 237 || javaLangPackageMatcher.matches(); 238 } 239 240 /** 241 * Collects references made by IDENT. 242 * 243 * @param ast the IDENT node to process 244 */ 245 private void processIdent(DetailAST ast) { 246 final DetailAST parent = ast.getParent(); 247 final int parentType = parent.getType(); 248 if (parentType != TokenTypes.DOT 249 && parentType != TokenTypes.METHOD_DEF 250 || parentType == TokenTypes.DOT 251 && ast.getNextSibling() != null) { 252 referenced.add(ast.getText()); 253 } 254 } 255 256 /** 257 * Collects the details of imports. 258 * 259 * @param ast node containing the import details 260 */ 261 private void processImport(DetailAST ast) { 262 final FullIdent name = FullIdent.createFullIdentBelow(ast); 263 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 264 imports.add(name); 265 } 266 } 267 268 /** 269 * Collects the details of static imports. 270 * 271 * @param ast node containing the static import details 272 */ 273 private void processStaticImport(DetailAST ast) { 274 final FullIdent name = 275 FullIdent.createFullIdent( 276 ast.getFirstChild().getNextSibling()); 277 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 278 imports.add(name); 279 } 280 } 281 282 /** 283 * Collects references made in Javadoc comments. 284 * 285 * @param ast node to inspect for Javadoc 286 */ 287 private void collectReferencesFromJavadoc(DetailAST ast) { 288 final FileContents contents = getFileContents(); 289 final int lineNo = ast.getLineNo(); 290 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 291 if (textBlock != null) { 292 referenced.addAll(collectReferencesFromJavadoc(textBlock)); 293 } 294 } 295 296 /** 297 * Process a javadoc {@link TextBlock} and return the set of classes 298 * referenced within. 299 * 300 * @param textBlock The javadoc block to parse 301 * @return a set of classes referenced in the javadoc block 302 */ 303 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 304 final List<JavadocTag> tags = new ArrayList<>(); 305 // gather all the inline tags, like @link 306 // INLINE tags inside BLOCKs get hidden when using ALL 307 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE)); 308 // gather all the block-level tags, like @throws and @see 309 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK)); 310 311 final Set<String> references = new HashSet<>(); 312 313 tags.stream() 314 .filter(JavadocTag::canReferenceImports) 315 .forEach(tag -> references.addAll(processJavadocTag(tag))); 316 return references; 317 } 318 319 /** 320 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 321 * 322 * @param cmt The javadoc block to parse 323 * @param tagType The type of tags we're interested in 324 * @return the list of tags 325 */ 326 private static List<JavadocTag> getValidTags(TextBlock cmt, 327 JavadocUtil.JavadocTagType tagType) { 328 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags(); 329 } 330 331 /** 332 * Returns a list of references found in a javadoc {@link JavadocTag}. 333 * 334 * @param tag The javadoc tag to parse 335 * @return A list of references found in this tag 336 */ 337 private static Set<String> processJavadocTag(JavadocTag tag) { 338 final Set<String> references = new HashSet<>(); 339 final String identifier = tag.getFirstArg().trim(); 340 for (Pattern pattern : new Pattern[] 341 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 342 references.addAll(matchPattern(identifier, pattern)); 343 } 344 return references; 345 } 346 347 /** 348 * Extracts a list of texts matching a {@link Pattern} from a 349 * {@link String}. 350 * 351 * @param identifier The String to match the pattern against 352 * @param pattern The Pattern used to extract the texts 353 * @return A list of texts which matched the pattern 354 */ 355 private static Set<String> matchPattern(String identifier, Pattern pattern) { 356 final Set<String> references = new HashSet<>(); 357 final Matcher matcher = pattern.matcher(identifier); 358 while (matcher.find()) { 359 references.add(topLevelType(matcher.group(1))); 360 } 361 return references; 362 } 363 364 /** 365 * If the given type string contains "." (e.g. "Map.Entry"), returns the 366 * top level type (e.g. "Map"), as that is what must be imported for the 367 * type to resolve. Otherwise, returns the type as-is. 368 * 369 * @param type A possibly qualified type name 370 * @return The simple name of the top level type 371 */ 372 private static String topLevelType(String type) { 373 final String topLevelType; 374 final int dotIndex = type.indexOf('.'); 375 if (dotIndex == -1) { 376 topLevelType = type; 377 } 378 else { 379 topLevelType = type.substring(0, dotIndex); 380 } 381 return topLevelType; 382 } 383 384}