001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.DetailNode; 033import com.puppycrawl.tools.checkstyle.api.FullIdent; 034import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 039import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 040 041/** 042 * <div> 043 * Checks for unused import statements. An import statement 044 * is considered unused if: 045 * </div> 046 * 047 * <ul> 048 * <li> 049 * It is not referenced in the file. The algorithm does not support wild-card 050 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated 051 * checks for imports that handle wild-card imports. 052 * </li> 053 * <li> 054 * The class imported is from the {@code java.lang} package. For example 055 * importing {@code java.lang.String}. 056 * </li> 057 * <li> 058 * The class imported is from the same package. 059 * </li> 060 * <li> 061 * A static method is imported when used as method reference. In that case, 062 * only the type needs to be imported and that's enough to resolve the method. 063 * </li> 064 * <li> 065 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by 066 * default, but it is considered bad practice to introduce a compile-time 067 * dependency for documentation purposes only. As an example, the import 068 * {@code java.util.Set} would be considered referenced with the Javadoc 069 * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time 070 * dependency would be to write the Javadoc comment as {@code {@link Set}}. 071 * </li> 072 * </ul> 073 * 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 static imports with the same method name 083 * (javac can distinguish imports with same name but different parameters, but checkstyle can not 084 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>) 085 * </li> 086 * <li> 087 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot 088 * determine which packages or types are brought into scope by an {@code import module} declaration. 089 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a> 090 * </li> 091 * </ul> 092 * 093 * @since 3.0 094 */ 095@FileStatefulCheck 096@SuppressWarnings("UnrecognisedJavadocTag") 097public class UnusedImportsCheck extends AbstractJavadocCheck { 098 099 /** 100 * A key is pointing to the warning message text in "messages.properties" 101 * file. 102 */ 103 public static final String MSG_KEY = "import.unused"; 104 105 /** Regexp pattern to match java.lang package. */ 106 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 107 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 108 109 /** Suffix for the star import. */ 110 private static final String STAR_IMPORT_SUFFIX = ".*"; 111 112 /** Prefix for wildcard extends bound. */ 113 private static final String WILDCARD_EXTENDS_PREFIX = "? extends "; 114 115 /** Prefix for wildcard super bound. */ 116 private static final String WILDCARD_SUPER_PREFIX = "? super "; 117 118 /** Pattern for a valid Java identifier (parameter name). */ 119 private static final Pattern PARAM_NAME_PATTERN = 120 Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*"); 121 122 /** Set of the imports. */ 123 private final Set<FullIdent> imports = new HashSet<>(); 124 125 /** Control whether to process Javadoc comments. */ 126 private boolean processJavadoc = true; 127 128 /** 129 * The scope is being processed. 130 * Types declared in a scope can shadow imported types. 131 */ 132 private Frame currentFrame; 133 134 /** 135 * Setter to control whether to process Javadoc comments. 136 * 137 * @param value Flag for processing Javadoc comments. 138 * @since 5.4 139 */ 140 public void setProcessJavadoc(boolean value) { 141 processJavadoc = value; 142 } 143 144 /** 145 * Setter to control when to print violations if the Javadoc being examined by this check 146 * violates the tight html rules defined at 147 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 148 * Tight-HTML Rules</a>. 149 * 150 * @param shouldReportViolation value to which the field shall be set to 151 * @since 8.3 152 * @propertySince 13.4.0 153 */ 154 @Override 155 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) { 156 super.setViolateExecutionOnNonTightHtml(shouldReportViolation); 157 } 158 159 @Override 160 public void beginTree(DetailAST rootAST) { 161 super.beginTree(rootAST); 162 currentFrame = Frame.compilationUnit(); 163 imports.clear(); 164 } 165 166 @Override 167 public void finishTree(DetailAST rootAST) { 168 currentFrame.finish(); 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[] getRequiredJavadocTokens() { 177 return new int[] { 178 JavadocCommentsTokenTypes.REFERENCE, 179 JavadocCommentsTokenTypes.PARAMETER_TYPE, 180 JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 181 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG, 182 }; 183 } 184 185 @Override 186 public int[] getDefaultJavadocTokens() { 187 return getRequiredJavadocTokens(); 188 } 189 190 @Override 191 public void visitJavadocToken(DetailNode ast) { 192 switch (ast.getType()) { 193 case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast); 194 case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast); 195 case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 196 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast); 197 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast); 198 } 199 200 } 201 202 @Override 203 public int[] getDefaultTokens() { 204 return getRequiredTokens(); 205 } 206 207 @Override 208 public int[] getAcceptableTokens() { 209 return getRequiredTokens(); 210 } 211 212 @Override 213 public int[] getRequiredTokens() { 214 return new int[] { 215 TokenTypes.IDENT, 216 TokenTypes.IMPORT, 217 TokenTypes.STATIC_IMPORT, 218 // Tokens for creating a new frame 219 TokenTypes.OBJBLOCK, 220 TokenTypes.SLIST, 221 // Javadoc 222 TokenTypes.BLOCK_COMMENT_BEGIN, 223 }; 224 } 225 226 @Override 227 public void visitToken(DetailAST ast) { 228 switch (ast.getType()) { 229 case TokenTypes.IDENT -> processIdent(ast); 230 case TokenTypes.IMPORT -> processImport(ast); 231 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast); 232 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push(); 233 case TokenTypes.BLOCK_COMMENT_BEGIN -> { 234 if (processJavadoc) { 235 super.visitToken(ast); 236 } 237 } 238 default -> throw new IllegalArgumentException("Unknown token type " + ast); 239 } 240 } 241 242 @Override 243 public void leaveToken(DetailAST ast) { 244 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 245 currentFrame = currentFrame.pop(); 246 } 247 } 248 249 /** 250 * Checks whether an import is unused. 251 * 252 * @param imprt an import. 253 * @return true if an import is unused. 254 */ 255 private boolean isUnusedImport(String imprt) { 256 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 257 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 258 || javaLangPackageMatcher.matches(); 259 } 260 261 /** 262 * Collects references made by IDENT. 263 * 264 * @param ast the IDENT node to process 265 */ 266 private void processIdent(DetailAST ast) { 267 final DetailAST parent = ast.getParent(); 268 final int parentType = parent.getType(); 269 270 // Ignore IDENTs that are part of the import statement itself 271 final boolean collect = parentType != TokenTypes.IMPORT 272 && parentType != TokenTypes.STATIC_IMPORT; 273 274 if (collect) { 275 final boolean isClassOrMethod = parentType == TokenTypes.DOT 276 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF; 277 278 if (TokenUtil.isTypeDeclaration(parentType)) { 279 currentFrame.addDeclaredType(ast.getText()); 280 } 281 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) { 282 currentFrame.addReferencedType(ast.getText()); 283 } 284 } 285 } 286 287 /** 288 * Checks whether ast is a fully qualified identifier. 289 * 290 * @param ast to check 291 * @return true if given ast is a fully qualified identifier 292 */ 293 private static boolean isQualifiedIdentifier(DetailAST ast) { 294 final DetailAST parent = ast.getParent(); 295 final int parentType = parent.getType(); 296 297 final boolean isQualifiedIdent = parentType == TokenTypes.DOT 298 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT) 299 && ast.getNextSibling() != null; 300 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF 301 && ast.getNextSibling() != null; 302 return isQualifiedIdent || isQualifiedIdentFromMethodRef; 303 } 304 305 /** 306 * Collects the details of imports. 307 * 308 * @param ast node containing the import details 309 */ 310 private void processImport(DetailAST ast) { 311 final FullIdent name = FullIdent.createFullIdentBelow(ast); 312 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 313 imports.add(name); 314 } 315 } 316 317 /** 318 * Collects the details of static imports. 319 * 320 * @param ast node containing the static import details 321 */ 322 private void processStaticImport(DetailAST ast) { 323 final FullIdent name = 324 FullIdent.createFullIdent( 325 ast.getFirstChild().getNextSibling()); 326 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 327 imports.add(name); 328 } 329 } 330 331 /** 332 * Processes a Javadoc reference to record referenced types. 333 * 334 * @param ast the Javadoc reference node 335 */ 336 private void processReference(DetailNode ast) { 337 final String referenceText = topLevelType(ast.getFirstChild().getText()); 338 currentFrame.addReferencedType(referenceText); 339 } 340 341 /** 342 * Processes a Javadoc parameter type tag to record referenced type. 343 * 344 * @param ast the Javadoc parameter type node 345 */ 346 private void processParameterType(DetailNode ast) { 347 addReferencedTypesFromType(ast.getText()); 348 } 349 350 /** 351 * Registers all type names referenced in a type string. 352 * Handles generic type arguments, wildcard bounds, and array suffixes. 353 * 354 * @param type the type string to process 355 */ 356 private void addReferencedTypesFromType(String type) { 357 String currentType = type; 358 if (currentType.startsWith(WILDCARD_EXTENDS_PREFIX)) { 359 currentType = currentType.substring(WILDCARD_EXTENDS_PREFIX.length()); 360 } 361 else if (currentType.startsWith(WILDCARD_SUPER_PREFIX)) { 362 currentType = currentType.substring(WILDCARD_SUPER_PREFIX.length()); 363 } 364 else { 365 currentType = stripTrailingParameterName(currentType); 366 } 367 if (currentType.endsWith("[]")) { 368 currentType = currentType.substring(0, currentType.length() - 2); 369 } 370 String outerType = stripTypeArguments(currentType); 371 outerType = stripTrailingGt(outerType); 372 outerType = topLevelType(outerType); 373 currentFrame.addReferencedType(outerType); 374 final int openIndex = currentType.indexOf('<'); 375 if (openIndex != -1) { 376 final int closeIndex = findMatchingCloseAngle(currentType, openIndex); 377 if (closeIndex != -1) { 378 final String typeArgs = currentType.substring(openIndex + 1, closeIndex); 379 for (String arg : splitTypeArguments(typeArgs)) { 380 addReferencedTypesFromType(arg); 381 } 382 } 383 } 384 } 385 386 /** 387 * Processes a Javadoc throws or exception tag to record referenced type. 388 * 389 * @param ast the Javadoc throws or exception node 390 */ 391 private void processException(DetailNode ast) { 392 final DetailNode ident = 393 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER); 394 if (ident != null) { 395 currentFrame.addReferencedType(ident.getText()); 396 } 397 } 398 399 /** 400 * If the given type string contains "." (e.g. "Map.Entry"), returns the 401 * top level type (e.g. "Map"), as that is what must be imported for the 402 * type to resolve. Otherwise, returns the type as-is. 403 * 404 * @param type A possibly qualified type name 405 * @return The simple name of the top level type 406 */ 407 private static String topLevelType(String type) { 408 String result = type; 409 final int dotIndex = type.indexOf('.'); 410 if (dotIndex != -1) { 411 result = type.substring(0, dotIndex); 412 } 413 return result; 414 } 415 416 /** 417 * Strips generic type arguments from a type string. 418 * 419 * @param type A type string possibly containing type arguments 420 * @return The type string with type arguments removed 421 */ 422 private static String stripTypeArguments(final String type) { 423 final int index = type.indexOf('<'); 424 final String result; 425 if (index == -1) { 426 result = type; 427 } 428 else { 429 result = type.substring(0, index); 430 } 431 return result; 432 } 433 434 /** 435 * Strips trailing {@code >} characters from a type string. 436 * This handles tokenization artifacts where the closing angle bracket 437 * of an enclosing generic is attached to the last parameter type. 438 * 439 * @param type A type string possibly ending with {@code >} 440 * @return The type string with trailing {@code >} characters removed 441 */ 442 private static String stripTrailingGt(String type) { 443 String result = type; 444 while (result.endsWith(">")) { 445 result = result.substring(0, result.length() - 1); 446 } 447 return result; 448 } 449 450 /** 451 * Strips a trailing parameter name (e.g. "outputTarget" in 452 * "Result outputTarget") from a type token when the 453 * Javadoc lexer merges the type and parameter name into a 454 * single PARAMETER_TYPE token. 455 * 456 * <p>Only strips if the substring after the last space is a valid 457 * Java identifier, which is true for a parameter name but false 458 * for generic content such as {@code BigDecimal>} in 459 * {@code Class<? extends BigDecimal>}.</p> 460 * 461 * @param type the raw token text 462 * @return the type portion with any trailing parameter name removed 463 */ 464 private static String stripTrailingParameterName(String type) { 465 final int lastSpace = type.lastIndexOf(' '); 466 String result = type; 467 if (lastSpace != -1) { 468 final String after = type.substring(lastSpace + 1); 469 if (PARAM_NAME_PATTERN.matcher(after).matches()) { 470 result = type.substring(0, lastSpace); 471 } 472 } 473 return result; 474 } 475 476 /** 477 * Finds the matching close angle bracket for the angle bracket at the given index. 478 * Correctly handles nested angle brackets by tracking depth. 479 * 480 * @param str the string to search in 481 * @param openIndex the index of the opening angle bracket 482 * @return the index of the matching close angle bracket, or -1 if not found 483 */ 484 private static int findMatchingCloseAngle(String str, int openIndex) { 485 int depth = 0; 486 int result = -1; 487 for (int idx = openIndex; idx < str.length(); idx++) { 488 if (str.charAt(idx) == '<') { 489 depth++; 490 } 491 else if (str.charAt(idx) == '>') { 492 depth--; 493 if (depth == 0) { 494 result = idx; 495 break; 496 } 497 } 498 } 499 return result; 500 } 501 502 /** 503 * Splits a type argument string into individual type argument strings. 504 * Comma handling is deferred to a follow-up issue that absorbs commas into 505 * PARAMETER_TYPE; currently commas are token boundaries so only one type 506 * argument ever appears here. 507 * 508 * @param typeArgs the type argument string (content between angle brackets) 509 * @return the list of individual type argument strings 510 */ 511 private static List<String> splitTypeArguments(String typeArgs) { 512 final List<String> result = new ArrayList<>(); 513 result.add(typeArgs); 514 return result; 515 } 516 517 /** 518 * Holds the names of referenced types and names of declared inner types. 519 */ 520 private static final class Frame { 521 522 /** Parent frame. */ 523 private final Frame parent; 524 525 /** Nested types declared in the current scope. */ 526 private final Set<String> declaredTypes; 527 528 /** Set of references - possibly to imports or locally declared types. */ 529 private final Set<String> referencedTypes; 530 531 /** 532 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 533 * 534 * @param parent the parent frame 535 */ 536 private Frame(Frame parent) { 537 this.parent = parent; 538 declaredTypes = new HashSet<>(); 539 referencedTypes = new HashSet<>(); 540 } 541 542 /** 543 * Adds new inner type. 544 * 545 * @param type the type name 546 */ 547 /* package */ void addDeclaredType(String type) { 548 declaredTypes.add(type); 549 } 550 551 /** 552 * Adds new type reference to the current frame. 553 * 554 * @param type the type name 555 */ 556 /* package */ void addReferencedType(String type) { 557 referencedTypes.add(type); 558 } 559 560 /** 561 * Adds new inner types. 562 * 563 * @param types the type names 564 */ 565 /* package */ void addReferencedTypes(Collection<String> types) { 566 referencedTypes.addAll(types); 567 } 568 569 /** 570 * Filters out all references to locally defined types. 571 * 572 */ 573 /* package */ void finish() { 574 referencedTypes.removeAll(declaredTypes); 575 } 576 577 /** 578 * Creates new inner frame. 579 * 580 * @return a new frame. 581 */ 582 /* package */ Frame push() { 583 return new Frame(this); 584 } 585 586 /** 587 * Pulls all referenced types up, except those that are declared in this scope. 588 * 589 * @return the parent frame 590 */ 591 /* package */ Frame pop() { 592 finish(); 593 parent.addReferencedTypes(referencedTypes); 594 return parent; 595 } 596 597 /** 598 * Checks whether this type name is used in this frame. 599 * 600 * @param type the type name 601 * @return {@code true} if the type is used 602 */ 603 /* package */ boolean isReferencedType(String type) { 604 return referencedTypes.contains(type); 605 } 606 607 /** 608 * Creates a new top-level frame for the compilation unit. 609 * 610 * @return a new frame. 611 */ 612 /* package */ static Frame compilationUnit() { 613 return new Frame(null); 614 } 615 616 } 617 618}