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