001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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 org.checkerframework.checker.index.qual.IndexOrLow; 032 033import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 035import com.puppycrawl.tools.checkstyle.api.DetailAST; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.FullIdent; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.api.TokenTypes; 040import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 041import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 042import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 043import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 044 045/** 046 * <div> 047 * Checks for unused import statements. An import statement 048 * is considered unused if: 049 * </div> 050 * 051 * <ul> 052 * <li> 053 * It is not referenced in the file. The algorithm does not support wild-card 054 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated 055 * checks for imports that handle wild-card imports. 056 * </li> 057 * <li> 058 * The class imported is from the {@code java.lang} package. For example 059 * importing {@code java.lang.String}. 060 * </li> 061 * <li> 062 * The class imported is from the same package. 063 * </li> 064 * <li> 065 * A static method is imported when used as method reference. In that case, 066 * only the type needs to be imported and that's enough to resolve the method. 067 * </li> 068 * <li> 069 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by 070 * default, but it is considered bad practice to introduce a compile-time 071 * dependency for documentation purposes only. As an example, the import 072 * {@code java.util.List} would be considered referenced with the Javadoc 073 * comment {@code {@link List}}. The alternative to avoid introducing a compile-time 074 * dependency would be to write the Javadoc comment as {@code {@link java.util.List}}. 075 * </li> 076 * </ul> 077 * 078 * <p> 079 * The main limitation of this check is handling the cases where: 080 * </p> 081 * <ul> 082 * <li> 083 * An imported type has the same name as a declaration, such as a member variable. 084 * </li> 085 * <li> 086 * There are two or more static imports with the same method name 087 * (javac can distinguish imports with same name but different parameters, but checkstyle can not 088 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>) 089 * </li> 090 * </ul> 091 * <ul> 092 * <li> 093 * Property {@code processJavadoc} - Control whether to process Javadoc comments. 094 * Type is {@code boolean}. 095 * Default value is {@code true}. 096 * </li> 097 * </ul> 098 * 099 * <p> 100 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 101 * </p> 102 * 103 * <p> 104 * Violation Message Keys: 105 * </p> 106 * <ul> 107 * <li> 108 * {@code import.unused} 109 * </li> 110 * </ul> 111 * 112 * @since 3.0 113 */ 114@FileStatefulCheck 115public class UnusedImportsCheck extends AbstractCheck { 116 117 /** 118 * A key is pointing to the warning message text in "messages.properties" 119 * file. 120 */ 121 public static final String MSG_KEY = "import.unused"; 122 123 /** Regex to match class names. */ 124 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 125 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 126 /** Regex to match the first class name. */ 127 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 128 "^" + CLASS_NAME); 129 /** Regex to match argument names. */ 130 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 131 "[(,]\\s*" + CLASS_NAME.pattern()); 132 133 /** Regexp pattern to match java.lang package. */ 134 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 135 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 136 137 /** Reference pattern. */ 138 private static final Pattern REFERENCE = Pattern.compile( 139 "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$", 140 Pattern.CASE_INSENSITIVE 141 ); 142 143 /** Method pattern. */ 144 private static final Pattern METHOD = Pattern.compile( 145 "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$", 146 Pattern.CASE_INSENSITIVE 147 ); 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 * @since 5.4 171 */ 172 public void setProcessJavadoc(boolean value) { 173 processJavadoc = value; 174 } 175 176 @Override 177 public void beginTree(DetailAST rootAST) { 178 collect = false; 179 currentFrame = Frame.compilationUnit(); 180 imports.clear(); 181 } 182 183 @Override 184 public void finishTree(DetailAST rootAST) { 185 currentFrame.finish(); 186 // loop over all the imports to see if referenced. 187 imports.stream() 188 .filter(imprt -> isUnusedImport(imprt.getText())) 189 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 190 } 191 192 @Override 193 public int[] getDefaultTokens() { 194 return getRequiredTokens(); 195 } 196 197 @Override 198 public int[] getRequiredTokens() { 199 return new int[] { 200 TokenTypes.IDENT, 201 TokenTypes.IMPORT, 202 TokenTypes.STATIC_IMPORT, 203 // Definitions that may contain Javadoc... 204 TokenTypes.PACKAGE_DEF, 205 TokenTypes.ANNOTATION_DEF, 206 TokenTypes.ANNOTATION_FIELD_DEF, 207 TokenTypes.ENUM_DEF, 208 TokenTypes.ENUM_CONSTANT_DEF, 209 TokenTypes.CLASS_DEF, 210 TokenTypes.INTERFACE_DEF, 211 TokenTypes.METHOD_DEF, 212 TokenTypes.CTOR_DEF, 213 TokenTypes.VARIABLE_DEF, 214 TokenTypes.RECORD_DEF, 215 TokenTypes.COMPACT_CTOR_DEF, 216 // Tokens for creating a new frame 217 TokenTypes.OBJBLOCK, 218 TokenTypes.SLIST, 219 }; 220 } 221 222 @Override 223 public int[] getAcceptableTokens() { 224 return getRequiredTokens(); 225 } 226 227 @Override 228 public void visitToken(DetailAST ast) { 229 switch (ast.getType()) { 230 case TokenTypes.IDENT: 231 if (collect) { 232 processIdent(ast); 233 } 234 break; 235 case TokenTypes.IMPORT: 236 processImport(ast); 237 break; 238 case TokenTypes.STATIC_IMPORT: 239 processStaticImport(ast); 240 break; 241 case TokenTypes.OBJBLOCK: 242 case TokenTypes.SLIST: 243 currentFrame = currentFrame.push(); 244 break; 245 default: 246 collect = true; 247 if (processJavadoc) { 248 collectReferencesFromJavadoc(ast); 249 } 250 break; 251 } 252 } 253 254 @Override 255 public void leaveToken(DetailAST ast) { 256 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 257 currentFrame = currentFrame.pop(); 258 } 259 } 260 261 /** 262 * Checks whether an import is unused. 263 * 264 * @param imprt an import. 265 * @return true if an import is unused. 266 */ 267 private boolean isUnusedImport(String imprt) { 268 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 269 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 270 || javaLangPackageMatcher.matches(); 271 } 272 273 /** 274 * Collects references made by IDENT. 275 * 276 * @param ast the IDENT node to process 277 */ 278 private void processIdent(DetailAST ast) { 279 final DetailAST parent = ast.getParent(); 280 final int parentType = parent.getType(); 281 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 * 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 * Collects references made in Javadoc comments. 339 * 340 * @param ast node to inspect for Javadoc 341 */ 342 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 343 @SuppressWarnings("deprecation") 344 private void collectReferencesFromJavadoc(DetailAST ast) { 345 final FileContents contents = getFileContents(); 346 final int lineNo = ast.getLineNo(); 347 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 348 if (textBlock != null) { 349 currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock)); 350 } 351 } 352 353 /** 354 * Process a javadoc {@link TextBlock} and return the set of classes 355 * referenced within. 356 * 357 * @param textBlock The javadoc block to parse 358 * @return a set of classes referenced in the javadoc block 359 */ 360 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 361 // Process INLINE tags 362 final List<JavadocTag> inlineTags = getTargetTags(textBlock, 363 JavadocUtil.JavadocTagType.INLINE); 364 // Process BLOCK tags 365 final List<JavadocTag> blockTags = getTargetTags(textBlock, 366 JavadocUtil.JavadocTagType.BLOCK); 367 final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream()) 368 .toList(); 369 370 final Set<String> references = new HashSet<>(); 371 372 targetTags.stream() 373 .filter(JavadocTag::canReferenceImports) 374 .forEach(tag -> references.addAll(processJavadocTag(tag))); 375 return references; 376 } 377 378 /** 379 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 380 * Filters tags based on whether they are inline or block tags, ensuring they match 381 * the correct format supported. 382 * 383 * @param cmt The javadoc block to parse 384 * @param javadocTagType The type of tags we're interested in 385 * @return the list of tags 386 */ 387 private static List<JavadocTag> getTargetTags(TextBlock cmt, 388 JavadocUtil.JavadocTagType javadocTagType) { 389 return JavadocUtil.getJavadocTags(cmt, javadocTagType) 390 .getValidTags() 391 .stream() 392 .filter(tag -> isMatchingTagType(tag, javadocTagType)) 393 .map(UnusedImportsCheck::bestTryToMatchReference) 394 .flatMap(Optional::stream) 395 .toList(); 396 } 397 398 /** 399 * Returns a list of references that found in a javadoc {@link JavadocTag}. 400 * 401 * @param tag The javadoc tag to parse 402 * @return A list of references that found in this tag 403 */ 404 private static Set<String> processJavadocTag(JavadocTag tag) { 405 final Set<String> references = new HashSet<>(); 406 final String identifier = tag.getFirstArg(); 407 for (Pattern pattern : new Pattern[] 408 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 409 references.addAll(matchPattern(identifier, pattern)); 410 } 411 return references; 412 } 413 414 /** 415 * Extracts a set of texts matching a {@link Pattern} from a 416 * {@link String}. 417 * 418 * @param identifier The String to match the pattern against 419 * @param pattern The Pattern used to extract the texts 420 * @return A set of texts which matched the pattern 421 */ 422 private static Set<String> matchPattern(String identifier, Pattern pattern) { 423 final Set<String> references = new HashSet<>(); 424 final Matcher matcher = pattern.matcher(identifier); 425 while (matcher.find()) { 426 references.add(topLevelType(matcher.group(1))); 427 } 428 return references; 429 } 430 431 /** 432 * If the given type string contains "." (e.g. "Map.Entry"), returns the 433 * top level type (e.g. "Map"), as that is what must be imported for the 434 * type to resolve. Otherwise, returns the type as-is. 435 * 436 * @param type A possibly qualified type name 437 * @return The simple name of the top level type 438 */ 439 private static String topLevelType(String type) { 440 final String topLevelType; 441 final int dotIndex = type.indexOf('.'); 442 if (dotIndex == -1) { 443 topLevelType = type; 444 } 445 else { 446 topLevelType = type.substring(0, dotIndex); 447 } 448 return topLevelType; 449 } 450 451 /** 452 * Checks if a Javadoc tag matches the expected type based on its extraction format. 453 * This method checks if an inline tag is extracted as a block tag or vice versa. 454 * It ensures that block tags are correctly recognized as block tags and inline tags 455 * as inline tags during processing. 456 * 457 * @param tag The Javadoc tag to check. 458 * @param javadocTagType The expected type of the tag (BLOCK or INLINE). 459 * @return {@code true} if the tag matches the expected type, otherwise {@code false}. 460 */ 461 private static boolean isMatchingTagType(JavadocTag tag, 462 JavadocUtil.JavadocTagType javadocTagType) { 463 final boolean isInlineTag = tag.isInlineTag(); 464 final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK; 465 466 return isBlockTagType != isInlineTag; 467 } 468 469 /** 470 * Attempts to match a reference string against a predefined pattern 471 * and extracts valid reference. 472 * 473 * @param tag the input tag to check 474 * @return Optional of extracted references 475 */ 476 public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) { 477 final String content = tag.getFirstArg(); 478 final int referenceIndex = extractReferencePart(content); 479 Optional<JavadocTag> validTag = Optional.empty(); 480 481 if (referenceIndex != -1) { 482 final String referenceString; 483 if (referenceIndex == 0) { 484 referenceString = content; 485 } 486 else { 487 referenceString = content.substring(0, referenceIndex); 488 } 489 final Matcher matcher = REFERENCE.matcher(referenceString); 490 if (matcher.matches()) { 491 final int methodIndex = 3; 492 final String methodPart = matcher.group(methodIndex); 493 final boolean isValid = methodPart == null 494 || METHOD.matcher(methodPart).matches(); 495 if (isValid) { 496 validTag = Optional.of(tag); 497 } 498 } 499 } 500 return validTag; 501 } 502 503 /** 504 * Extracts the reference part from an input string while ensuring balanced parentheses. 505 * 506 * @param input the input string 507 * @return -1 if parentheses are unbalanced, 0 if no method is found, 508 * or the index of the first space outside parentheses. 509 */ 510 private static @IndexOrLow("#1")int extractReferencePart(String input) { 511 int parenthesesCount = 0; 512 int firstSpaceOutsideParens = -1; 513 for (int index = 0; index < input.length(); index++) { 514 final char currentCharacter = input.charAt(index); 515 516 if (currentCharacter == '(') { 517 parenthesesCount++; 518 } 519 else if (currentCharacter == ')') { 520 parenthesesCount--; 521 } 522 else if (currentCharacter == ' ' && parenthesesCount == 0) { 523 firstSpaceOutsideParens = index; 524 break; 525 } 526 } 527 528 int methodIndex = -1; 529 if (parenthesesCount == 0) { 530 if (firstSpaceOutsideParens == -1) { 531 methodIndex = 0; 532 } 533 else { 534 methodIndex = firstSpaceOutsideParens; 535 } 536 } 537 return methodIndex; 538 } 539 540 /** 541 * Holds the names of referenced types and names of declared inner types. 542 */ 543 private static final class Frame { 544 545 /** Parent frame. */ 546 private final Frame parent; 547 548 /** Nested types declared in the current scope. */ 549 private final Set<String> declaredTypes; 550 551 /** Set of references - possibly to imports or locally declared types. */ 552 private final Set<String> referencedTypes; 553 554 /** 555 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 556 * 557 * @param parent the parent frame 558 */ 559 private Frame(Frame parent) { 560 this.parent = parent; 561 declaredTypes = new HashSet<>(); 562 referencedTypes = new HashSet<>(); 563 } 564 565 /** 566 * Adds new inner type. 567 * 568 * @param type the type name 569 */ 570 public void addDeclaredType(String type) { 571 declaredTypes.add(type); 572 } 573 574 /** 575 * Adds new type reference to the current frame. 576 * 577 * @param type the type name 578 */ 579 public void addReferencedType(String type) { 580 referencedTypes.add(type); 581 } 582 583 /** 584 * Adds new inner types. 585 * 586 * @param types the type names 587 */ 588 public void addReferencedTypes(Collection<String> types) { 589 referencedTypes.addAll(types); 590 } 591 592 /** 593 * Filters out all references to locally defined types. 594 * 595 */ 596 public void finish() { 597 referencedTypes.removeAll(declaredTypes); 598 } 599 600 /** 601 * Creates new inner frame. 602 * 603 * @return a new frame. 604 */ 605 public Frame push() { 606 return new Frame(this); 607 } 608 609 /** 610 * Pulls all referenced types up, except those that are declared in this scope. 611 * 612 * @return the parent frame 613 */ 614 public Frame pop() { 615 finish(); 616 parent.addReferencedTypes(referencedTypes); 617 return parent; 618 } 619 620 /** 621 * Checks whether this type name is used in this frame. 622 * 623 * @param type the type name 624 * @return {@code true} if the type is used 625 */ 626 public boolean isReferencedType(String type) { 627 return referencedTypes.contains(type); 628 } 629 630 /** 631 * Creates a new top-level frame for the compilation unit. 632 * 633 * @return a new frame. 634 */ 635 public static Frame compilationUnit() { 636 return new Frame(null); 637 } 638 639 } 640 641}