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.Set; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.DetailNode; 031import com.puppycrawl.tools.checkstyle.api.FullIdent; 032import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 033import com.puppycrawl.tools.checkstyle.api.TokenTypes; 034import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 035import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 036import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 037import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 038 039/** 040 * <div> 041 * Checks for unused import statements. An import statement 042 * is considered unused if: 043 * </div> 044 * 045 * <ul> 046 * <li> 047 * It is not referenced in the file. The algorithm does not support wild-card 048 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated 049 * checks for imports that handle wild-card imports. 050 * </li> 051 * <li> 052 * The class imported is from the {@code java.lang} package. For example 053 * importing {@code java.lang.String}. 054 * </li> 055 * <li> 056 * The class imported is from the same package. 057 * </li> 058 * <li> 059 * A static method is imported when used as method reference. In that case, 060 * only the type needs to be imported and that's enough to resolve the method. 061 * </li> 062 * <li> 063 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by 064 * default, but it is considered bad practice to introduce a compile-time 065 * dependency for documentation purposes only. As an example, the import 066 * {@code java.util.Set} would be considered referenced with the Javadoc 067 * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time 068 * dependency would be to write the Javadoc comment as {@code {@link Set}}. 069 * </li> 070 * </ul> 071 * 072 * <p> 073 * The main limitation of this check is handling the cases where: 074 * </p> 075 * <ul> 076 * <li> 077 * An imported type has the same name as a declaration, such as a member variable. 078 * </li> 079 * <li> 080 * There are two or more static imports with the same method name 081 * (javac can distinguish imports with same name but different parameters, but checkstyle can not 082 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>) 083 * </li> 084 * <li> 085 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot 086 * determine which packages or types are brought into scope by an {@code import module} declaration. 087 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a> 088 * </li> 089 * </ul> 090 * 091 * @since 3.0 092 */ 093@FileStatefulCheck 094@SuppressWarnings("UnrecognisedJavadocTag") 095public class UnusedImportsCheck extends AbstractJavadocCheck { 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 /** Regexp pattern to match java.lang package. */ 104 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 105 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 106 107 /** Suffix for the star import. */ 108 private static final String STAR_IMPORT_SUFFIX = ".*"; 109 110 /** Set of the imports. */ 111 private final Set<FullIdent> imports = new HashSet<>(); 112 113 /** Control whether to process Javadoc comments. */ 114 private boolean processJavadoc = true; 115 116 /** 117 * The scope is being processed. 118 * Types declared in a scope can shadow imported types. 119 */ 120 private Frame currentFrame; 121 122 /** 123 * Setter to control whether to process Javadoc comments. 124 * 125 * @param value Flag for processing Javadoc comments. 126 * @since 5.4 127 */ 128 public void setProcessJavadoc(boolean value) { 129 processJavadoc = value; 130 } 131 132 /** 133 * Setter to control when to print violations if the Javadoc being examined by this check 134 * violates the tight html rules defined at 135 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 136 * Tight-HTML Rules</a>. 137 * 138 * @param shouldReportViolation value to which the field shall be set to 139 * @since 8.3 140 * @propertySince 13.4.0 141 */ 142 @Override 143 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) { 144 super.setViolateExecutionOnNonTightHtml(shouldReportViolation); 145 } 146 147 @Override 148 public void beginTree(DetailAST rootAST) { 149 super.beginTree(rootAST); 150 currentFrame = Frame.compilationUnit(); 151 imports.clear(); 152 } 153 154 @Override 155 public void finishTree(DetailAST rootAST) { 156 currentFrame.finish(); 157 // loop over all the imports to see if referenced. 158 imports.stream() 159 .filter(imprt -> isUnusedImport(imprt.getText())) 160 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText())); 161 } 162 163 @Override 164 public int[] getRequiredJavadocTokens() { 165 return new int[] { 166 JavadocCommentsTokenTypes.REFERENCE, 167 JavadocCommentsTokenTypes.PARAMETER_TYPE, 168 JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 169 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG, 170 }; 171 } 172 173 @Override 174 public int[] getDefaultJavadocTokens() { 175 return getRequiredJavadocTokens(); 176 } 177 178 @Override 179 public void visitJavadocToken(DetailNode ast) { 180 switch (ast.getType()) { 181 case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast); 182 case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast); 183 case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG, 184 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast); 185 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast); 186 } 187 188 } 189 190 @Override 191 public int[] getDefaultTokens() { 192 return getRequiredTokens(); 193 } 194 195 @Override 196 public int[] getAcceptableTokens() { 197 return getRequiredTokens(); 198 } 199 200 @Override 201 public int[] getRequiredTokens() { 202 return new int[] { 203 TokenTypes.IDENT, 204 TokenTypes.IMPORT, 205 TokenTypes.STATIC_IMPORT, 206 // Tokens for creating a new frame 207 TokenTypes.OBJBLOCK, 208 TokenTypes.SLIST, 209 // Javadoc 210 TokenTypes.BLOCK_COMMENT_BEGIN, 211 }; 212 } 213 214 @Override 215 public void visitToken(DetailAST ast) { 216 switch (ast.getType()) { 217 case TokenTypes.IDENT -> processIdent(ast); 218 case TokenTypes.IMPORT -> processImport(ast); 219 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast); 220 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push(); 221 case TokenTypes.BLOCK_COMMENT_BEGIN -> { 222 if (processJavadoc) { 223 super.visitToken(ast); 224 } 225 } 226 default -> throw new IllegalArgumentException("Unknown token type " + ast); 227 } 228 } 229 230 @Override 231 public void leaveToken(DetailAST ast) { 232 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) { 233 currentFrame = currentFrame.pop(); 234 } 235 } 236 237 /** 238 * Checks whether an import is unused. 239 * 240 * @param imprt an import. 241 * @return true if an import is unused. 242 */ 243 private boolean isUnusedImport(String imprt) { 244 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 245 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt)) 246 || javaLangPackageMatcher.matches(); 247 } 248 249 /** 250 * Collects references made by IDENT. 251 * 252 * @param ast the IDENT node to process 253 */ 254 private void processIdent(DetailAST ast) { 255 final DetailAST parent = ast.getParent(); 256 final int parentType = parent.getType(); 257 258 // Ignore IDENTs that are part of the import statement itself 259 final boolean collect = parentType != TokenTypes.IMPORT 260 && parentType != TokenTypes.STATIC_IMPORT; 261 262 if (collect) { 263 final boolean isClassOrMethod = parentType == TokenTypes.DOT 264 || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF; 265 266 if (TokenUtil.isTypeDeclaration(parentType)) { 267 currentFrame.addDeclaredType(ast.getText()); 268 } 269 else if (!isClassOrMethod || isQualifiedIdentifier(ast)) { 270 currentFrame.addReferencedType(ast.getText()); 271 } 272 } 273 } 274 275 /** 276 * Checks whether ast is a fully qualified identifier. 277 * 278 * @param ast to check 279 * @return true if given ast is a fully qualified identifier 280 */ 281 private static boolean isQualifiedIdentifier(DetailAST ast) { 282 final DetailAST parent = ast.getParent(); 283 final int parentType = parent.getType(); 284 285 final boolean isQualifiedIdent = parentType == TokenTypes.DOT 286 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT) 287 && ast.getNextSibling() != null; 288 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF 289 && ast.getNextSibling() != null; 290 return isQualifiedIdent || isQualifiedIdentFromMethodRef; 291 } 292 293 /** 294 * Collects the details of imports. 295 * 296 * @param ast node containing the import details 297 */ 298 private void processImport(DetailAST ast) { 299 final FullIdent name = FullIdent.createFullIdentBelow(ast); 300 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 301 imports.add(name); 302 } 303 } 304 305 /** 306 * Collects the details of static imports. 307 * 308 * @param ast node containing the static import details 309 */ 310 private void processStaticImport(DetailAST ast) { 311 final FullIdent name = 312 FullIdent.createFullIdent( 313 ast.getFirstChild().getNextSibling()); 314 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 315 imports.add(name); 316 } 317 } 318 319 /** 320 * Processes a Javadoc reference to record referenced types. 321 * 322 * @param ast the Javadoc reference node 323 */ 324 private void processReference(DetailNode ast) { 325 final String referenceText = topLevelType(ast.getFirstChild().getText()); 326 currentFrame.addReferencedType(referenceText); 327 } 328 329 /** 330 * Processes a Javadoc parameter type tag to record referenced type. 331 * 332 * @param ast the Javadoc parameter type node 333 */ 334 private void processParameterType(DetailNode ast) { 335 String parameterTypeText = topLevelType(ast.getText()); 336 if (parameterTypeText.endsWith("[]")) { 337 parameterTypeText = parameterTypeText.substring(0, parameterTypeText.length() - 2); 338 } 339 currentFrame.addReferencedType(parameterTypeText); 340 } 341 342 /** 343 * Processes a Javadoc throws or exception tag to record referenced type. 344 * 345 * @param ast the Javadoc throws or exception node 346 */ 347 private void processException(DetailNode ast) { 348 final DetailNode ident = 349 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER); 350 if (ident != null) { 351 currentFrame.addReferencedType(ident.getText()); 352 } 353 } 354 355 /** 356 * If the given type string contains "." (e.g. "Map.Entry"), returns the 357 * top level type (e.g. "Map"), as that is what must be imported for the 358 * type to resolve. Otherwise, returns the type as-is. 359 * 360 * @param type A possibly qualified type name 361 * @return The simple name of the top level type 362 */ 363 private static String topLevelType(String type) { 364 String result = type; 365 final int dotIndex = type.indexOf('.'); 366 if (dotIndex != -1) { 367 result = type.substring(0, dotIndex); 368 } 369 return result; 370 } 371 372 /** 373 * Holds the names of referenced types and names of declared inner types. 374 */ 375 private static final class Frame { 376 377 /** Parent frame. */ 378 private final Frame parent; 379 380 /** Nested types declared in the current scope. */ 381 private final Set<String> declaredTypes; 382 383 /** Set of references - possibly to imports or locally declared types. */ 384 private final Set<String> referencedTypes; 385 386 /** 387 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame. 388 * 389 * @param parent the parent frame 390 */ 391 private Frame(Frame parent) { 392 this.parent = parent; 393 declaredTypes = new HashSet<>(); 394 referencedTypes = new HashSet<>(); 395 } 396 397 /** 398 * Adds new inner type. 399 * 400 * @param type the type name 401 */ 402 /* package */ void addDeclaredType(String type) { 403 declaredTypes.add(type); 404 } 405 406 /** 407 * Adds new type reference to the current frame. 408 * 409 * @param type the type name 410 */ 411 /* package */ void addReferencedType(String type) { 412 referencedTypes.add(type); 413 } 414 415 /** 416 * Adds new inner types. 417 * 418 * @param types the type names 419 */ 420 /* package */ void addReferencedTypes(Collection<String> types) { 421 referencedTypes.addAll(types); 422 } 423 424 /** 425 * Filters out all references to locally defined types. 426 * 427 */ 428 /* package */ void finish() { 429 referencedTypes.removeAll(declaredTypes); 430 } 431 432 /** 433 * Creates new inner frame. 434 * 435 * @return a new frame. 436 */ 437 /* package */ Frame push() { 438 return new Frame(this); 439 } 440 441 /** 442 * Pulls all referenced types up, except those that are declared in this scope. 443 * 444 * @return the parent frame 445 */ 446 /* package */ Frame pop() { 447 finish(); 448 parent.addReferencedTypes(referencedTypes); 449 return parent; 450 } 451 452 /** 453 * Checks whether this type name is used in this frame. 454 * 455 * @param type the type name 456 * @return {@code true} if the type is used 457 */ 458 /* package */ boolean isReferencedType(String type) { 459 return referencedTypes.contains(type); 460 } 461 462 /** 463 * Creates a new top-level frame for the compilation unit. 464 * 465 * @return a new frame. 466 */ 467 /* package */ static Frame compilationUnit() { 468 return new Frame(null); 469 } 470 471 } 472 473}