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