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 * Creates a new {@code UnusedImportsCheck} instance. 136 */ 137 public UnusedImportsCheck() { 138 // no code by default 139 } 140 141 /** 142 * Setter to control whether to process Javadoc comments. 143 * 144 * @param value Flag for processing Javadoc comments. 145 * @since 5.4 146 */ 147 public void setProcessJavadoc(boolean value) { 148 processJavadoc = value; 149 } 150 151 /** 152 * Setter to control when to print violations if the Javadoc being examined by this check 153 * violates the tight html rules defined at 154 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 155 * Tight-HTML Rules</a>. 156 * 157 * @param shouldReportViolation value to which the field shall be set to 158 * @since 8.3 159 * @propertySince 13.4.0 160 */ 161 @Override 162 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) { 163 super.setViolateExecutionOnNonTightHtml(shouldReportViolation); 164 } 165 166 @Override 167 public void beginTree(DetailAST rootAST) { 168 super.beginTree(rootAST); 169 currentFrame = Frame.compilationUnit(); 170 imports.clear(); 171 } 172 173 @Override 174 public void finishTree(DetailAST rootAST) { 175 currentFrame.finish(); 176 // loop over all the imports to see if referenced. 177 imports.stream() 178 .filter(imprt -> isUnusedImport(imprt.getText())) 179 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 180 } 181 182 @Override 183 public int[] getRequiredJavadocTokens() { 184 return new int[] { 185 JavadocCommentsTokenTypes.REFERENCE, 186 JavadocCommentsTokenTypes.PARAMETER_TYPE, 187 JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 188 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG, 189 }; 190 } 191 192 @Override 193 public int[] getDefaultJavadocTokens() { 194 return getRequiredJavadocTokens(); 195 } 196 197 @Override 198 public void visitJavadocToken(DetailNode ast) { 199 switch (ast.getType()) { 200 case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast); 201 case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast); 202 case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 203 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast); 204 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast); 205 } 206 207 } 208 209 @Override 210 public int[] getDefaultTokens() { 211 return getRequiredTokens(); 212 } 213 214 @Override 215 public int[] getAcceptableTokens() { 216 return getRequiredTokens(); 217 } 218 219 @Override 220 public int[] getRequiredTokens() { 221 return new int[] { 222 TokenTypes.IDENT, 223 TokenTypes.IMPORT, 224 TokenTypes.STATIC_IMPORT, 225 // Tokens for creating a new frame 226 TokenTypes.OBJBLOCK, 227 TokenTypes.SLIST, 228 // Javadoc 229 TokenTypes.BLOCK_COMMENT_BEGIN, 230 }; 231 } 232 233 @Override 234 public void visitToken(DetailAST ast) { 235 switch (ast.getType()) { 236 case TokenTypes.IDENT -> processIdent(ast); 237 case TokenTypes.IMPORT -> processImport(ast); 238 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast); 239 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push(); 240 case TokenTypes.BLOCK_COMMENT_BEGIN -> { 241 if (processJavadoc) { 242 super.visitToken(ast); 243 } 244 } 245 default -> throw new IllegalArgumentException("Unknown token type " + ast); 246 } 247 } 248 249 @Override 250 public void leaveToken(DetailAST ast) { 251 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 252 currentFrame = currentFrame.pop(); 253 } 254 } 255 256 /** 257 * Checks whether an import is unused. 258 * 259 * @param imprt an import. 260 * @return true if an import is unused. 261 */ 262 private boolean isUnusedImport(String imprt) { 263 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 264 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 265 || javaLangPackageMatcher.matches(); 266 } 267 268 /** 269 * Collects references made by IDENT. 270 * 271 * @param ast the IDENT node to process 272 */ 273 private void processIdent(DetailAST ast) { 274 final DetailAST parent = ast.getParent(); 275 final int parentType = parent.getType(); 276 277 // Ignore IDENTs that are part of the import statement itself 278 final boolean collect = parentType != TokenTypes.IMPORT 279 && parentType != TokenTypes.STATIC_IMPORT; 280 281 if (collect) { 282 final boolean isClassOrMethod = parentType == TokenTypes.DOT 283 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF; 284 285 if (TokenUtil.isTypeDeclaration(parentType)) { 286 currentFrame.addDeclaredType(ast.getText()); 287 } 288 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) { 289 currentFrame.addReferencedType(ast.getText()); 290 } 291 } 292 } 293 294 /** 295 * Checks whether ast is a fully qualified identifier. 296 * 297 * @param ast to check 298 * @return true if given ast is a fully qualified identifier 299 */ 300 private static boolean isQualifiedIdentifier(DetailAST ast) { 301 final DetailAST parent = ast.getParent(); 302 final int parentType = parent.getType(); 303 304 final boolean isQualifiedIdent = parentType == TokenTypes.DOT 305 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT) 306 && ast.getNextSibling() != null; 307 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF 308 && ast.getNextSibling() != null; 309 return isQualifiedIdent || isQualifiedIdentFromMethodRef; 310 } 311 312 /** 313 * Collects the details of imports. 314 * 315 * @param ast node containing the import details 316 */ 317 private void processImport(DetailAST ast) { 318 final FullIdent name = FullIdent.createFullIdentBelow(ast); 319 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 320 imports.add(name); 321 } 322 } 323 324 /** 325 * Collects the details of static imports. 326 * 327 * @param ast node containing the static import details 328 */ 329 private void processStaticImport(DetailAST ast) { 330 final FullIdent name = 331 FullIdent.createFullIdent( 332 ast.getFirstChild().getNextSibling()); 333 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 334 imports.add(name); 335 } 336 } 337 338 /** 339 * Processes a Javadoc reference to record referenced types. 340 * 341 * @param ast the Javadoc reference node 342 */ 343 private void processReference(DetailNode ast) { 344 final String referenceText = topLevelType(ast.getFirstChild().getText()); 345 currentFrame.addReferencedType(referenceText); 346 } 347 348 /** 349 * Processes a Javadoc parameter type tag to record referenced type. 350 * 351 * @param ast the Javadoc parameter type node 352 */ 353 private void processParameterType(DetailNode ast) { 354 addReferencedTypesFromType(ast.getText()); 355 } 356 357 /** 358 * Registers all type names referenced in a type string. 359 * Handles generic type arguments, wildcard bounds, and array suffixes. 360 * 361 * @param type the type string to process 362 */ 363 private void addReferencedTypesFromType(String type) { 364 String currentType = type; 365 if (currentType.startsWith(WILDCARD_EXTENDS_PREFIX)) { 366 currentType = currentType.substring(WILDCARD_EXTENDS_PREFIX.length()); 367 } 368 else if (currentType.startsWith(WILDCARD_SUPER_PREFIX)) { 369 currentType = currentType.substring(WILDCARD_SUPER_PREFIX.length()); 370 } 371 else { 372 currentType = stripTrailingParameterName(currentType); 373 } 374 if (currentType.endsWith("[]")) { 375 currentType = currentType.substring(0, currentType.length() - 2); 376 } 377 String outerType = stripTypeArguments(currentType); 378 outerType = stripTrailingGt(outerType); 379 outerType = topLevelType(outerType); 380 currentFrame.addReferencedType(outerType); 381 final int openIndex = currentType.indexOf('<'); 382 if (openIndex != -1) { 383 final int closeIndex = findMatchingCloseAngle(currentType, openIndex); 384 if (closeIndex != -1) { 385 final String typeArgs = currentType.substring(openIndex + 1, closeIndex); 386 for (String arg : splitTypeArguments(typeArgs)) { 387 addReferencedTypesFromType(arg); 388 } 389 } 390 } 391 } 392 393 /** 394 * Processes a Javadoc throws or exception tag to record referenced type. 395 * 396 * @param ast the Javadoc throws or exception node 397 */ 398 private void processException(DetailNode ast) { 399 final DetailNode ident = 400 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER); 401 if (ident != null) { 402 currentFrame.addReferencedType(ident.getText()); 403 } 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 String result = type; 416 final int dotIndex = type.indexOf('.'); 417 if (dotIndex != -1) { 418 result = type.substring(0, dotIndex); 419 } 420 return result; 421 } 422 423 /** 424 * Strips generic type arguments from a type string. 425 * 426 * @param type A type string possibly containing type arguments 427 * @return The type string with type arguments removed 428 */ 429 private static String stripTypeArguments(final String type) { 430 final int index = type.indexOf('<'); 431 final String result; 432 if (index == -1) { 433 result = type; 434 } 435 else { 436 result = type.substring(0, index); 437 } 438 return result; 439 } 440 441 /** 442 * Strips trailing {@code >} characters from a type string. 443 * This handles tokenization artifacts where the closing angle bracket 444 * of an enclosing generic is attached to the last parameter type. 445 * 446 * @param type A type string possibly ending with {@code >} 447 * @return The type string with trailing {@code >} characters removed 448 */ 449 private static String stripTrailingGt(String type) { 450 String result = type; 451 while (result.endsWith(">")) { 452 result = result.substring(0, result.length() - 1); 453 } 454 return result; 455 } 456 457 /** 458 * Strips a trailing parameter name (e.g. "outputTarget" in 459 * "Result outputTarget") from a type token when the 460 * Javadoc lexer merges the type and parameter name into a 461 * single PARAMETER_TYPE token. 462 * 463 * <p>Only strips if the substring after the last space is a valid 464 * Java identifier, which is true for a parameter name but false 465 * for generic content such as {@code BigDecimal>} in 466 * {@code Class<? extends BigDecimal>}.</p> 467 * 468 * @param type the raw token text 469 * @return the type portion with any trailing parameter name removed 470 */ 471 private static String stripTrailingParameterName(String type) { 472 final int lastSpace = type.lastIndexOf(' '); 473 String result = type; 474 if (lastSpace != -1) { 475 final String after = type.substring(lastSpace + 1); 476 if (PARAM_NAME_PATTERN.matcher(after).matches()) { 477 result = type.substring(0, lastSpace); 478 } 479 } 480 return result; 481 } 482 483 /** 484 * Finds the matching close angle bracket for the angle bracket at the given index. 485 * Correctly handles nested angle brackets by tracking depth. 486 * 487 * @param str the string to search in 488 * @param openIndex the index of the opening angle bracket 489 * @return the index of the matching close angle bracket, or -1 if not found 490 */ 491 private static int findMatchingCloseAngle(String str, int openIndex) { 492 int depth = 0; 493 int result = -1; 494 for (int idx = openIndex; idx < str.length(); idx++) { 495 if (str.charAt(idx) == '<') { 496 depth++; 497 } 498 else if (str.charAt(idx) == '>') { 499 depth--; 500 if (depth == 0) { 501 result = idx; 502 break; 503 } 504 } 505 } 506 return result; 507 } 508 509 /** 510 * Splits a type argument string into individual type argument strings. 511 * Comma handling is deferred to a follow-up issue that absorbs commas into 512 * PARAMETER_TYPE; currently commas are token boundaries so only one type 513 * argument ever appears here. 514 * 515 * @param typeArgs the type argument string (content between angle brackets) 516 * @return the list of individual type argument strings 517 */ 518 private static List<String> splitTypeArguments(String typeArgs) { 519 final List<String> result = new ArrayList<>(); 520 result.add(typeArgs); 521 return result; 522 } 523 524 /** 525 * Holds the names of referenced types and names of declared inner types. 526 */ 527 private static final class Frame { 528 529 /** Parent frame. */ 530 private final Frame parent; 531 532 /** Nested types declared in the current scope. */ 533 private final Set<String> declaredTypes; 534 535 /** Set of references - possibly to imports or locally declared types. */ 536 private final Set<String> referencedTypes; 537 538 /** 539 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 540 * 541 * @param parent the parent frame 542 */ 543 private Frame(Frame parent) { 544 this.parent = parent; 545 declaredTypes = new HashSet<>(); 546 referencedTypes = new HashSet<>(); 547 } 548 549 /** 550 * Adds new inner type. 551 * 552 * @param type the type name 553 */ 554 /* package */ void addDeclaredType(String type) { 555 declaredTypes.add(type); 556 } 557 558 /** 559 * Adds new type reference to the current frame. 560 * 561 * @param type the type name 562 */ 563 /* package */ void addReferencedType(String type) { 564 referencedTypes.add(type); 565 } 566 567 /** 568 * Adds new inner types. 569 * 570 * @param types the type names 571 */ 572 /* package */ void addReferencedTypes(Collection<String> types) { 573 referencedTypes.addAll(types); 574 } 575 576 /** 577 * Filters out all references to locally defined types. 578 * 579 */ 580 /* package */ void finish() { 581 referencedTypes.removeAll(declaredTypes); 582 } 583 584 /** 585 * Creates new inner frame. 586 * 587 * @return a new frame. 588 */ 589 /* package */ Frame push() { 590 return new Frame(this); 591 } 592 593 /** 594 * Pulls all referenced types up, except those that are declared in this scope. 595 * 596 * @return the parent frame 597 */ 598 /* package */ Frame pop() { 599 finish(); 600 parent.addReferencedTypes(referencedTypes); 601 return parent; 602 } 603 604 /** 605 * Checks whether this type name is used in this frame. 606 * 607 * @param type the type name 608 * @return {@code true} if the type is used 609 */ 610 /* package */ boolean isReferencedType(String type) { 611 return referencedTypes.contains(type); 612 } 613 614 /** 615 * Creates a new top-level frame for the compilation unit. 616 * 617 * @return a new frame. 618 */ 619 /* package */ static Frame compilationUnit() { 620 return new Frame(null); 621 } 622 623 } 624 625}