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 * <li> 091 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot 092 * determine which packages or types are brought into scope by an {@code import module} declaration. 093 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a> 094 * </li> 095 * </ul> 096 * 097 * @since 3.0 098 */ 099@FileStatefulCheck 100public class UnusedImportsCheck extends AbstractCheck { 101 102 /** 103 * A key is pointing to the warning message text in "messages.properties" 104 * file. 105 */ 106 public static final String MSG_KEY = "import.unused"; 107 108 /** Regex to match class names. */ 109 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 110 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 111 /** Regex to match the first class name. */ 112 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 113 "^" + CLASS_NAME); 114 /** Regex to match argument names. */ 115 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 116 "[(,]\\s*" + CLASS_NAME.pattern()); 117 118 /** Regexp pattern to match java.lang package. */ 119 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 120 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 121 122 /** Reference pattern. */ 123 private static final Pattern REFERENCE = Pattern.compile( 124 "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$", 125 Pattern.CASE_INSENSITIVE 126 ); 127 128 /** Method pattern. */ 129 private static final Pattern METHOD = Pattern.compile( 130 "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$", 131 Pattern.CASE_INSENSITIVE 132 ); 133 134 /** Suffix for the star import. */ 135 private static final String STAR_IMPORT_SUFFIX = ".*"; 136 137 /** Set of the imports. */ 138 private final Set<FullIdent> imports = new HashSet<>(); 139 140 /** Flag to indicate when time to start collecting references. */ 141 private boolean collect; 142 /** Control whether to process Javadoc comments. */ 143 private boolean processJavadoc = true; 144 145 /** 146 * The scope is being processed. 147 * Types declared in a scope can shadow imported types. 148 */ 149 private Frame currentFrame; 150 151 /** 152 * Setter to control whether to process Javadoc comments. 153 * 154 * @param value Flag for processing Javadoc comments. 155 * @since 5.4 156 */ 157 public void setProcessJavadoc(boolean value) { 158 processJavadoc = value; 159 } 160 161 @Override 162 public void beginTree(DetailAST rootAST) { 163 collect = false; 164 currentFrame = Frame.compilationUnit(); 165 imports.clear(); 166 } 167 168 @Override 169 public void finishTree(DetailAST rootAST) { 170 currentFrame.finish(); 171 // loop over all the imports to see if referenced. 172 imports.stream() 173 .filter(imprt -> isUnusedImport(imprt.getText())) 174 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 175 } 176 177 @Override 178 public int[] getDefaultTokens() { 179 return getRequiredTokens(); 180 } 181 182 @Override 183 public int[] getRequiredTokens() { 184 return new int[] { 185 TokenTypes.IDENT, 186 TokenTypes.IMPORT, 187 TokenTypes.STATIC_IMPORT, 188 // Definitions that may contain Javadoc... 189 TokenTypes.PACKAGE_DEF, 190 TokenTypes.ANNOTATION_DEF, 191 TokenTypes.ANNOTATION_FIELD_DEF, 192 TokenTypes.ENUM_DEF, 193 TokenTypes.ENUM_CONSTANT_DEF, 194 TokenTypes.CLASS_DEF, 195 TokenTypes.INTERFACE_DEF, 196 TokenTypes.METHOD_DEF, 197 TokenTypes.CTOR_DEF, 198 TokenTypes.VARIABLE_DEF, 199 TokenTypes.RECORD_DEF, 200 TokenTypes.COMPACT_CTOR_DEF, 201 // Tokens for creating a new frame 202 TokenTypes.OBJBLOCK, 203 TokenTypes.SLIST, 204 }; 205 } 206 207 @Override 208 public int[] getAcceptableTokens() { 209 return getRequiredTokens(); 210 } 211 212 @Override 213 public void visitToken(DetailAST ast) { 214 switch (ast.getType()) { 215 case TokenTypes.IDENT -> { 216 if (collect) { 217 processIdent(ast); 218 } 219 } 220 case TokenTypes.IMPORT -> processImport(ast); 221 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast); 222 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push(); 223 default -> { 224 collect = true; 225 if (processJavadoc) { 226 collectReferencesFromJavadoc(ast); 227 } 228 } 229 } 230 } 231 232 @Override 233 public void leaveToken(DetailAST ast) { 234 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 235 currentFrame = currentFrame.pop(); 236 } 237 } 238 239 /** 240 * Checks whether an import is unused. 241 * 242 * @param imprt an import. 243 * @return true if an import is unused. 244 */ 245 private boolean isUnusedImport(String imprt) { 246 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 247 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 248 || javaLangPackageMatcher.matches(); 249 } 250 251 /** 252 * Collects references made by IDENT. 253 * 254 * @param ast the IDENT node to process 255 */ 256 private void processIdent(DetailAST ast) { 257 final DetailAST parent = ast.getParent(); 258 final int parentType = parent.getType(); 259 260 final boolean isClassOrMethod = parentType == TokenTypes.DOT 261 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF; 262 263 if (TokenUtil.isTypeDeclaration(parentType)) { 264 currentFrame.addDeclaredType(ast.getText()); 265 } 266 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) { 267 currentFrame.addReferencedType(ast.getText()); 268 } 269 } 270 271 /** 272 * Checks whether ast is a fully qualified identifier. 273 * 274 * @param ast to check 275 * @return true if given ast is a fully qualified identifier 276 */ 277 private static boolean isQualifiedIdentifier(DetailAST ast) { 278 final DetailAST parent = ast.getParent(); 279 final int parentType = parent.getType(); 280 281 final boolean isQualifiedIdent = parentType == TokenTypes.DOT 282 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT) 283 && ast.getNextSibling() != null; 284 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF 285 && ast.getNextSibling() != null; 286 return isQualifiedIdent || isQualifiedIdentFromMethodRef; 287 } 288 289 /** 290 * Collects the details of imports. 291 * 292 * @param ast node containing the import details 293 */ 294 private void processImport(DetailAST ast) { 295 final FullIdent name = FullIdent.createFullIdentBelow(ast); 296 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 297 imports.add(name); 298 } 299 } 300 301 /** 302 * Collects the details of static imports. 303 * 304 * @param ast node containing the static import details 305 */ 306 private void processStaticImport(DetailAST ast) { 307 final FullIdent name = 308 FullIdent.createFullIdent( 309 ast.getFirstChild().getNextSibling()); 310 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 311 imports.add(name); 312 } 313 } 314 315 /** 316 * Collects references made in Javadoc comments. 317 * 318 * @param ast node to inspect for Javadoc 319 */ 320 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 321 @SuppressWarnings("deprecation") 322 private void collectReferencesFromJavadoc(DetailAST ast) { 323 final FileContents contents = getFileContents(); 324 final int lineNo = ast.getLineNo(); 325 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 326 if (textBlock != null) { 327 currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock)); 328 } 329 } 330 331 /** 332 * Process a javadoc {@link TextBlock} and return the set of classes 333 * referenced within. 334 * 335 * @param textBlock The javadoc block to parse 336 * @return a set of classes referenced in the javadoc block 337 */ 338 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 339 // Process INLINE tags 340 final List<JavadocTag> inlineTags = getTargetTags(textBlock, 341 JavadocUtil.JavadocTagType.INLINE); 342 // Process BLOCK tags 343 final List<JavadocTag> blockTags = getTargetTags(textBlock, 344 JavadocUtil.JavadocTagType.BLOCK); 345 final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream()) 346 .toList(); 347 348 final Set<String> references = new HashSet<>(); 349 350 targetTags.stream() 351 .filter(JavadocTag::canReferenceImports) 352 .forEach(tag -> references.addAll(processJavadocTag(tag))); 353 return references; 354 } 355 356 /** 357 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 358 * Filters tags based on whether they are inline or block tags, ensuring they match 359 * the correct format supported. 360 * 361 * @param cmt The javadoc block to parse 362 * @param javadocTagType The type of tags we're interested in 363 * @return the list of tags 364 */ 365 private static List<JavadocTag> getTargetTags(TextBlock cmt, 366 JavadocUtil.JavadocTagType javadocTagType) { 367 return JavadocUtil.getJavadocTags(cmt, javadocTagType) 368 .getValidTags() 369 .stream() 370 .filter(tag -> isMatchingTagType(tag, javadocTagType)) 371 .map(UnusedImportsCheck::bestTryToMatchReference) 372 .flatMap(Optional::stream) 373 .toList(); 374 } 375 376 /** 377 * Returns a list of references that found in a javadoc {@link JavadocTag}. 378 * 379 * @param tag The javadoc tag to parse 380 * @return A list of references that found in this tag 381 */ 382 private static Set<String> processJavadocTag(JavadocTag tag) { 383 final Set<String> references = new HashSet<>(); 384 final String identifier = tag.getFirstArg(); 385 for (Pattern pattern : new Pattern[] 386 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 387 references.addAll(matchPattern(identifier, pattern)); 388 } 389 return references; 390 } 391 392 /** 393 * Extracts a set of texts matching a {@link Pattern} from a 394 * {@link String}. 395 * 396 * @param identifier The String to match the pattern against 397 * @param pattern The Pattern used to extract the texts 398 * @return A set of texts which matched the pattern 399 */ 400 private static Set<String> matchPattern(String identifier, Pattern pattern) { 401 final Set<String> references = new HashSet<>(); 402 final Matcher matcher = pattern.matcher(identifier); 403 while (matcher.find()) { 404 references.add(topLevelType(matcher.group(1))); 405 } 406 return references; 407 } 408 409 /** 410 * If the given type string contains "." (e.g. "Map.Entry"), returns the 411 * top level type (e.g. "Map"), as that is what must be imported for the 412 * type to resolve. Otherwise, returns the type as-is. 413 * 414 * @param type A possibly qualified type name 415 * @return The simple name of the top level type 416 */ 417 private static String topLevelType(String type) { 418 final String topLevelType; 419 final int dotIndex = type.indexOf('.'); 420 if (dotIndex == -1) { 421 topLevelType = type; 422 } 423 else { 424 topLevelType = type.substring(0, dotIndex); 425 } 426 return topLevelType; 427 } 428 429 /** 430 * Checks if a Javadoc tag matches the expected type based on its extraction format. 431 * This method checks if an inline tag is extracted as a block tag or vice versa. 432 * It ensures that block tags are correctly recognized as block tags and inline tags 433 * as inline tags during processing. 434 * 435 * @param tag The Javadoc tag to check. 436 * @param javadocTagType The expected type of the tag (BLOCK or INLINE). 437 * @return {@code true} if the tag matches the expected type, otherwise {@code false}. 438 */ 439 private static boolean isMatchingTagType(JavadocTag tag, 440 JavadocUtil.JavadocTagType javadocTagType) { 441 final boolean isInlineTag = tag.isInlineTag(); 442 final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK; 443 444 return isBlockTagType != isInlineTag; 445 } 446 447 /** 448 * Attempts to match a reference string against a predefined pattern 449 * and extracts valid reference. 450 * 451 * @param tag the input tag to check 452 * @return Optional of extracted references 453 */ 454 public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) { 455 final String content = tag.getFirstArg(); 456 final int referenceIndex = extractReferencePart(content); 457 Optional<JavadocTag> validTag = Optional.empty(); 458 459 if (referenceIndex != -1) { 460 final String referenceString; 461 if (referenceIndex == 0) { 462 referenceString = content; 463 } 464 else { 465 referenceString = content.substring(0, referenceIndex); 466 } 467 final Matcher matcher = REFERENCE.matcher(referenceString); 468 if (matcher.matches()) { 469 final int methodIndex = 3; 470 final String methodPart = matcher.group(methodIndex); 471 final boolean isValid = methodPart == null 472 || METHOD.matcher(methodPart).matches(); 473 if (isValid) { 474 validTag = Optional.of(tag); 475 } 476 } 477 } 478 return validTag; 479 } 480 481 /** 482 * Extracts the reference part from an input string while ensuring balanced parentheses. 483 * 484 * @param input the input string 485 * @return -1 if parentheses are unbalanced, 0 if no method is found, 486 * or the index of the first space outside parentheses. 487 */ 488 private static @IndexOrLow("#1")int extractReferencePart(String input) { 489 int parenthesesCount = 0; 490 int firstSpaceOutsideParens = -1; 491 for (int index = 0; index < input.length(); index++) { 492 final char currentCharacter = input.charAt(index); 493 494 if (currentCharacter == '(') { 495 parenthesesCount++; 496 } 497 else if (currentCharacter == ')') { 498 parenthesesCount--; 499 } 500 else if (currentCharacter == ' ' && parenthesesCount == 0) { 501 firstSpaceOutsideParens = index; 502 break; 503 } 504 } 505 506 int methodIndex = -1; 507 if (parenthesesCount == 0) { 508 if (firstSpaceOutsideParens == -1) { 509 methodIndex = 0; 510 } 511 else { 512 methodIndex = firstSpaceOutsideParens; 513 } 514 } 515 return methodIndex; 516 } 517 518 /** 519 * Holds the names of referenced types and names of declared inner types. 520 */ 521 private static final class Frame { 522 523 /** Parent frame. */ 524 private final Frame parent; 525 526 /** Nested types declared in the current scope. */ 527 private final Set<String> declaredTypes; 528 529 /** Set of references - possibly to imports or locally declared types. */ 530 private final Set<String> referencedTypes; 531 532 /** 533 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 534 * 535 * @param parent the parent frame 536 */ 537 private Frame(Frame parent) { 538 this.parent = parent; 539 declaredTypes = new HashSet<>(); 540 referencedTypes = new HashSet<>(); 541 } 542 543 /** 544 * Adds new inner type. 545 * 546 * @param type the type name 547 */ 548 public void addDeclaredType(String type) { 549 declaredTypes.add(type); 550 } 551 552 /** 553 * Adds new type reference to the current frame. 554 * 555 * @param type the type name 556 */ 557 public void addReferencedType(String type) { 558 referencedTypes.add(type); 559 } 560 561 /** 562 * Adds new inner types. 563 * 564 * @param types the type names 565 */ 566 public void addReferencedTypes(Collection<String> types) { 567 referencedTypes.addAll(types); 568 } 569 570 /** 571 * Filters out all references to locally defined types. 572 * 573 */ 574 public void finish() { 575 referencedTypes.removeAll(declaredTypes); 576 } 577 578 /** 579 * Creates new inner frame. 580 * 581 * @return a new frame. 582 */ 583 public Frame push() { 584 return new Frame(this); 585 } 586 587 /** 588 * Pulls all referenced types up, except those that are declared in this scope. 589 * 590 * @return the parent frame 591 */ 592 public Frame pop() { 593 finish(); 594 parent.addReferencedTypes(referencedTypes); 595 return parent; 596 } 597 598 /** 599 * Checks whether this type name is used in this frame. 600 * 601 * @param type the type name 602 * @return {@code true} if the type is used 603 */ 604 public boolean isReferencedType(String type) { 605 return referencedTypes.contains(type); 606 } 607 608 /** 609 * Creates a new top-level frame for the compilation unit. 610 * 611 * @return a new frame. 612 */ 613 public static Frame compilationUnit() { 614 return new Frame(null); 615 } 616 617 } 618 619}