001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.Collection; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 031import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 032import com.puppycrawl.tools.checkstyle.api.DetailAST; 033import com.puppycrawl.tools.checkstyle.api.FileContents; 034import com.puppycrawl.tools.checkstyle.api.FullIdent; 035import com.puppycrawl.tools.checkstyle.api.TextBlock; 036import com.puppycrawl.tools.checkstyle.api.TokenTypes; 037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 038import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 040import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 041 042/** 043 * <p> 044 * Checks for unused import statements. Checkstyle uses a simple but very 045 * reliable algorithm to report on unused import statements. An import statement 046 * is considered unused if: 047 * </p> 048 * <ul> 049 * <li> 050 * It is not referenced in the file. The algorithm does not support wild-card 051 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated 052 * checks for imports that handle wild-card imports. 053 * </li> 054 * <li> 055 * It is a duplicate of another import. This is when a class is imported more 056 * than once. 057 * </li> 058 * <li> 059 * The class imported is from the {@code java.lang} package. For example 060 * importing {@code java.lang.String}. 061 * </li> 062 * <li> 063 * The class imported is from the same package. 064 * </li> 065 * <li> 066 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by 067 * default, but it is considered bad practice to introduce a compile-time 068 * dependency for documentation purposes only. As an example, the import 069 * {@code java.util.List} would be considered referenced with the Javadoc 070 * comment {@code {@link List}}. The alternative to avoid introducing a compile-time 071 * dependency would be to write the Javadoc comment as {@code {@link java.util.List}}. 072 * </li> 073 * </ul> 074 * <p> 075 * The main limitation of this check is handling the cases where: 076 * </p> 077 * <ul> 078 * <li> 079 * An imported type has the same name as a declaration, such as a member variable. 080 * </li> 081 * <li> 082 * There are two or more imports with the same name. 083 * </li> 084 * </ul> 085 * <p> 086 * For example, in the following case all imports will not be flagged as unused: 087 * </p> 088 * <pre> 089 * import java.awt.Component; 090 * import static AstTreeStringPrinter.printFileAst; 091 * import static DetailNodeTreeStringPrinter.printFileAst; 092 * class FooBar { 093 * private Object Component; // a bad practice in my opinion 094 * void method() { 095 * printFileAst(file); // two imports with the same name 096 * } 097 * } 098 * </pre> 099 * <ul> 100 * <li> 101 * Property {@code processJavadoc} - Control whether to process Javadoc comments. 102 * Type is {@code boolean}. 103 * Default value is {@code true}. 104 * </li> 105 * </ul> 106 * <p> 107 * To configure the check: 108 * </p> 109 * <pre> 110 * <module name="UnusedImports"/> 111 * </pre> 112 * <p> 113 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 114 * </p> 115 * <p> 116 * Violation Message Keys: 117 * </p> 118 * <ul> 119 * <li> 120 * {@code import.unused} 121 * </li> 122 * </ul> 123 * 124 * @since 3.0 125 */ 126@FileStatefulCheck 127public class UnusedImportsCheck extends AbstractCheck { 128 129 /** 130 * A key is pointing to the warning message text in "messages.properties" 131 * file. 132 */ 133 public static final String MSG_KEY = "import.unused"; 134 135 /** Regex to match class names. */ 136 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 137 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 138 /** Regex to match the first class name. */ 139 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 140 "^" + CLASS_NAME); 141 /** Regex to match argument names. */ 142 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 143 "[(,]\\s*" + CLASS_NAME.pattern()); 144 145 /** Regexp pattern to match java.lang package. */ 146 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 147 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 148 149 /** Suffix for the star import. */ 150 private static final String STAR_IMPORT_SUFFIX = ".*"; 151 152 /** Set of the imports. */ 153 private final Set<FullIdent> imports = new HashSet<>(); 154 155 /** Flag to indicate when time to start collecting references. */ 156 private boolean collect; 157 /** Control whether to process Javadoc comments. */ 158 private boolean processJavadoc = true; 159 160 /** 161 * The scope is being processed. 162 * Types declared in a scope can shadow imported types. 163 */ 164 private Frame currentFrame; 165 166 /** 167 * Setter to control whether to process Javadoc comments. 168 * 169 * @param value Flag for processing Javadoc comments. 170 */ 171 public void setProcessJavadoc(boolean value) { 172 processJavadoc = value; 173 } 174 175 @Override 176 public void beginTree(DetailAST rootAST) { 177 collect = false; 178 currentFrame = Frame.compilationUnit(); 179 imports.clear(); 180 } 181 182 @Override 183 public void finishTree(DetailAST rootAST) { 184 currentFrame.finish(); 185 // loop over all the imports to see if referenced. 186 imports.stream() 187 .filter(imprt -> isUnusedImport(imprt.getText())) 188 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 189 } 190 191 @Override 192 public int[] getDefaultTokens() { 193 return getRequiredTokens(); 194 } 195 196 @Override 197 public int[] getRequiredTokens() { 198 return new int[] { 199 TokenTypes.IDENT, 200 TokenTypes.IMPORT, 201 TokenTypes.STATIC_IMPORT, 202 // Definitions that may contain Javadoc... 203 TokenTypes.PACKAGE_DEF, 204 TokenTypes.ANNOTATION_DEF, 205 TokenTypes.ANNOTATION_FIELD_DEF, 206 TokenTypes.ENUM_DEF, 207 TokenTypes.ENUM_CONSTANT_DEF, 208 TokenTypes.CLASS_DEF, 209 TokenTypes.INTERFACE_DEF, 210 TokenTypes.METHOD_DEF, 211 TokenTypes.CTOR_DEF, 212 TokenTypes.VARIABLE_DEF, 213 TokenTypes.RECORD_DEF, 214 TokenTypes.COMPACT_CTOR_DEF, 215 // Tokens for creating a new frame 216 TokenTypes.OBJBLOCK, 217 TokenTypes.SLIST, 218 }; 219 } 220 221 @Override 222 public int[] getAcceptableTokens() { 223 return getRequiredTokens(); 224 } 225 226 @Override 227 public void visitToken(DetailAST ast) { 228 switch (ast.getType()) { 229 case TokenTypes.IDENT: 230 if (collect) { 231 processIdent(ast); 232 } 233 break; 234 case TokenTypes.IMPORT: 235 processImport(ast); 236 break; 237 case TokenTypes.STATIC_IMPORT: 238 processStaticImport(ast); 239 break; 240 case TokenTypes.OBJBLOCK: 241 case TokenTypes.SLIST: 242 currentFrame = currentFrame.push(); 243 break; 244 default: 245 collect = true; 246 if (processJavadoc) { 247 collectReferencesFromJavadoc(ast); 248 } 249 break; 250 } 251 } 252 253 @Override 254 public void leaveToken(DetailAST ast) { 255 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 256 currentFrame = currentFrame.pop(); 257 } 258 } 259 260 /** 261 * Checks whether an import is unused. 262 * 263 * @param imprt an import. 264 * @return true if an import is unused. 265 */ 266 private boolean isUnusedImport(String imprt) { 267 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 268 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 269 || javaLangPackageMatcher.matches(); 270 } 271 272 /** 273 * Collects references made by IDENT. 274 * 275 * @param ast the IDENT node to process 276 */ 277 private void processIdent(DetailAST ast) { 278 final DetailAST parent = ast.getParent(); 279 final int parentType = parent.getType(); 280 281 final boolean isPossibleDotClassOrInMethod = parentType == TokenTypes.DOT 282 || parentType == TokenTypes.METHOD_DEF; 283 284 final boolean isQualifiedIdent = parentType == TokenTypes.DOT 285 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT) 286 && ast.getNextSibling() != null; 287 288 if (TokenUtil.isTypeDeclaration(parentType)) { 289 currentFrame.addDeclaredType(ast.getText()); 290 } 291 else if (!isPossibleDotClassOrInMethod || isQualifiedIdent) { 292 currentFrame.addReferencedType(ast.getText()); 293 } 294 } 295 296 /** 297 * Collects the details of imports. 298 * 299 * @param ast node containing the import details 300 */ 301 private void processImport(DetailAST ast) { 302 final FullIdent name = FullIdent.createFullIdentBelow(ast); 303 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 304 imports.add(name); 305 } 306 } 307 308 /** 309 * Collects the details of static imports. 310 * 311 * @param ast node containing the static import details 312 */ 313 private void processStaticImport(DetailAST ast) { 314 final FullIdent name = 315 FullIdent.createFullIdent( 316 ast.getFirstChild().getNextSibling()); 317 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 318 imports.add(name); 319 } 320 } 321 322 /** 323 * Collects references made in Javadoc comments. 324 * 325 * @param ast node to inspect for Javadoc 326 */ 327 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 328 @SuppressWarnings("deprecation") 329 private void collectReferencesFromJavadoc(DetailAST ast) { 330 final FileContents contents = getFileContents(); 331 final int lineNo = ast.getLineNo(); 332 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 333 if (textBlock != null) { 334 currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock)); 335 } 336 } 337 338 /** 339 * Process a javadoc {@link TextBlock} and return the set of classes 340 * referenced within. 341 * 342 * @param textBlock The javadoc block to parse 343 * @return a set of classes referenced in the javadoc block 344 */ 345 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 346 final List<JavadocTag> tags = new ArrayList<>(); 347 // gather all the inline tags, like @link 348 // INLINE tags inside BLOCKs get hidden when using ALL 349 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE)); 350 // gather all the block-level tags, like @throws and @see 351 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK)); 352 353 final Set<String> references = new HashSet<>(); 354 355 tags.stream() 356 .filter(JavadocTag::canReferenceImports) 357 .forEach(tag -> references.addAll(processJavadocTag(tag))); 358 return references; 359 } 360 361 /** 362 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 363 * 364 * @param cmt The javadoc block to parse 365 * @param tagType The type of tags we're interested in 366 * @return the list of tags 367 */ 368 private static List<JavadocTag> getValidTags(TextBlock cmt, 369 JavadocUtil.JavadocTagType tagType) { 370 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags(); 371 } 372 373 /** 374 * Returns a list of references that found in a javadoc {@link JavadocTag}. 375 * 376 * @param tag The javadoc tag to parse 377 * @return A list of references that found in this tag 378 */ 379 private static Set<String> processJavadocTag(JavadocTag tag) { 380 final Set<String> references = new HashSet<>(); 381 final String identifier = tag.getFirstArg().trim(); 382 for (Pattern pattern : new Pattern[] 383 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 384 references.addAll(matchPattern(identifier, pattern)); 385 } 386 return references; 387 } 388 389 /** 390 * Extracts a set of texts matching a {@link Pattern} from a 391 * {@link String}. 392 * 393 * @param identifier The String to match the pattern against 394 * @param pattern The Pattern used to extract the texts 395 * @return A set of texts which matched the pattern 396 */ 397 private static Set<String> matchPattern(String identifier, Pattern pattern) { 398 final Set<String> references = new HashSet<>(); 399 final Matcher matcher = pattern.matcher(identifier); 400 while (matcher.find()) { 401 references.add(topLevelType(matcher.group(1))); 402 } 403 return references; 404 } 405 406 /** 407 * If the given type string contains "." (e.g. "Map.Entry"), returns the 408 * top level type (e.g. "Map"), as that is what must be imported for the 409 * type to resolve. Otherwise, returns the type as-is. 410 * 411 * @param type A possibly qualified type name 412 * @return The simple name of the top level type 413 */ 414 private static String topLevelType(String type) { 415 final String topLevelType; 416 final int dotIndex = type.indexOf('.'); 417 if (dotIndex == -1) { 418 topLevelType = type; 419 } 420 else { 421 topLevelType = type.substring(0, dotIndex); 422 } 423 return topLevelType; 424 } 425 426 /** 427 * Holds the names of referenced types and names of declared inner types. 428 */ 429 private static final class Frame { 430 431 /** Parent frame. */ 432 private final Frame parent; 433 434 /** Nested types declared in the current scope. */ 435 private final Set<String> declaredTypes; 436 437 /** Set of references - possibly to imports or locally declared types. */ 438 private final Set<String> referencedTypes; 439 440 /** 441 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 442 * 443 * @param parent the parent frame 444 */ 445 private Frame(Frame parent) { 446 this.parent = parent; 447 declaredTypes = new HashSet<>(); 448 referencedTypes = new HashSet<>(); 449 } 450 451 /** 452 * Adds new inner type. 453 * 454 * @param type the type name 455 */ 456 public void addDeclaredType(String type) { 457 declaredTypes.add(type); 458 } 459 460 /** 461 * Adds new type reference to the current frame. 462 * 463 * @param type the type name 464 */ 465 public void addReferencedType(String type) { 466 referencedTypes.add(type); 467 } 468 469 /** 470 * Adds new inner types. 471 * 472 * @param types the type names 473 */ 474 public void addReferencedTypes(Collection<String> types) { 475 referencedTypes.addAll(types); 476 } 477 478 /** 479 * Filters out all references to locally defined types. 480 * 481 */ 482 public void finish() { 483 referencedTypes.removeAll(declaredTypes); 484 } 485 486 /** 487 * Creates new inner frame. 488 * 489 * @return a new frame. 490 */ 491 public Frame push() { 492 return new Frame(this); 493 } 494 495 /** 496 * Pulls all referenced types up, except those that are declared in this scope. 497 * 498 * @return the parent frame 499 */ 500 public Frame pop() { 501 finish(); 502 parent.addReferencedTypes(referencedTypes); 503 return parent; 504 } 505 506 /** 507 * Checks whether this type name is used in this frame. 508 * 509 * @param type the type name 510 * @return {@code true} if the type is used 511 */ 512 public boolean isReferencedType(String type) { 513 return referencedTypes.contains(type); 514 } 515 516 /** 517 * Creates a new top-level frame for the compilation unit. 518 * 519 * @return a new frame. 520 */ 521 public static Frame compilationUnit() { 522 return new Frame(null); 523 } 524 525 } 526 527}