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