1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.imports;
21
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.DetailNode;
33 import com.puppycrawl.tools.checkstyle.api.FullIdent;
34 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
35 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36 import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
37 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
40
41 /**
42 * <div>
43 * Checks for unused import statements. An import statement
44 * is considered unused if:
45 * </div>
46 *
47 * <ul>
48 * <li>
49 * It is not referenced in the file. The algorithm does not support wild-card
50 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
51 * checks for imports that handle wild-card imports.
52 * </li>
53 * <li>
54 * The class imported is from the {@code java.lang} package. For example
55 * importing {@code java.lang.String}.
56 * </li>
57 * <li>
58 * The class imported is from the same package.
59 * </li>
60 * <li>
61 * A static method is imported when used as method reference. In that case,
62 * only the type needs to be imported and that's enough to resolve the method.
63 * </li>
64 * <li>
65 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
66 * default, but it is considered bad practice to introduce a compile-time
67 * dependency for documentation purposes only. As an example, the import
68 * {@code java.util.Set} would be considered referenced with the Javadoc
69 * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time
70 * dependency would be to write the Javadoc comment as {@code {@link Set}}.
71 * </li>
72 * </ul>
73 *
74 * <p>
75 * The main limitation of this check is handling the cases where:
76 * </p>
77 * <ul>
78 * <li>
79 * An imported type has the same name as a declaration, such as a member variable.
80 * </li>
81 * <li>
82 * There are two or more static imports with the same method name
83 * (javac can distinguish imports with same name but different parameters, but checkstyle can not
84 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
85 * </li>
86 * <li>
87 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
88 * determine which packages or types are brought into scope by an {@code import module} declaration.
89 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
90 * </li>
91 * </ul>
92 *
93 * @since 3.0
94 */
95 @FileStatefulCheck
96 @SuppressWarnings("UnrecognisedJavadocTag")
97 public class UnusedImportsCheck extends AbstractJavadocCheck {
98
99 /**
100 * A key is pointing to the warning message text in "messages.properties"
101 * file.
102 */
103 public static final String MSG_KEY = "import.unused";
104
105 /** Regexp pattern to match java.lang package. */
106 private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
107 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
108
109 /** Suffix for the star import. */
110 private static final String STAR_IMPORT_SUFFIX = ".*";
111
112 /** Prefix for wildcard extends bound. */
113 private static final String WILDCARD_EXTENDS_PREFIX = "? extends ";
114
115 /** Prefix for wildcard super bound. */
116 private static final String WILDCARD_SUPER_PREFIX = "? super ";
117
118 /** Pattern for a valid Java identifier (parameter name). */
119 private static final Pattern PARAM_NAME_PATTERN =
120 Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*");
121
122 /** Set of the imports. */
123 private final Set<FullIdent> imports = new HashSet<>();
124
125 /** Control whether to process Javadoc comments. */
126 private boolean processJavadoc = true;
127
128 /**
129 * The scope is being processed.
130 * Types declared in a scope can shadow imported types.
131 */
132 private Frame currentFrame;
133
134 /**
135 * Creates a new {@code UnusedImportsCheck} instance.
136 */
137 public UnusedImportsCheck() {
138 // no code by default
139 }
140
141 /**
142 * Setter to control whether to process Javadoc comments.
143 *
144 * @param value Flag for processing Javadoc comments.
145 * @since 5.4
146 */
147 public void setProcessJavadoc(boolean value) {
148 processJavadoc = value;
149 }
150
151 /**
152 * Setter to control when to print violations if the Javadoc being examined by this check
153 * violates the tight html rules defined at
154 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
155 * Tight-HTML Rules</a>.
156 *
157 * @param shouldReportViolation value to which the field shall be set to
158 * @since 8.3
159 * @propertySince 13.4.0
160 */
161 @Override
162 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
163 super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
164 }
165
166 @Override
167 public void beginTree(DetailAST rootAST) {
168 super.beginTree(rootAST);
169 currentFrame = Frame.compilationUnit();
170 imports.clear();
171 }
172
173 @Override
174 public void finishTree(DetailAST rootAST) {
175 currentFrame.finish();
176 // loop over all the imports to see if referenced.
177 imports.stream()
178 .filter(imprt -> isUnusedImport(imprt.getText()))
179 .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
180 }
181
182 @Override
183 public int[] getRequiredJavadocTokens() {
184 return new int[] {
185 JavadocCommentsTokenTypes.REFERENCE,
186 JavadocCommentsTokenTypes.PARAMETER_TYPE,
187 JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
188 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
189 };
190 }
191
192 @Override
193 public int[] getDefaultJavadocTokens() {
194 return getRequiredJavadocTokens();
195 }
196
197 @Override
198 public void visitJavadocToken(DetailNode ast) {
199 switch (ast.getType()) {
200 case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast);
201 case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast);
202 case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
203 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast);
204 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
205 }
206
207 }
208
209 @Override
210 public int[] getDefaultTokens() {
211 return getRequiredTokens();
212 }
213
214 @Override
215 public int[] getAcceptableTokens() {
216 return getRequiredTokens();
217 }
218
219 @Override
220 public int[] getRequiredTokens() {
221 return new int[] {
222 TokenTypes.IDENT,
223 TokenTypes.IMPORT,
224 TokenTypes.STATIC_IMPORT,
225 // Tokens for creating a new frame
226 TokenTypes.OBJBLOCK,
227 TokenTypes.SLIST,
228 // Javadoc
229 TokenTypes.BLOCK_COMMENT_BEGIN,
230 };
231 }
232
233 @Override
234 public void visitToken(DetailAST ast) {
235 switch (ast.getType()) {
236 case TokenTypes.IDENT -> processIdent(ast);
237 case TokenTypes.IMPORT -> processImport(ast);
238 case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
239 case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
240 case TokenTypes.BLOCK_COMMENT_BEGIN -> {
241 if (processJavadoc) {
242 super.visitToken(ast);
243 }
244 }
245 default -> throw new IllegalArgumentException("Unknown token type " + ast);
246 }
247 }
248
249 @Override
250 public void leaveToken(DetailAST ast) {
251 if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
252 currentFrame = currentFrame.pop();
253 }
254 }
255
256 /**
257 * Checks whether an import is unused.
258 *
259 * @param imprt an import.
260 * @return true if an import is unused.
261 */
262 private boolean isUnusedImport(String imprt) {
263 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
264 return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
265 || javaLangPackageMatcher.matches();
266 }
267
268 /**
269 * Collects references made by IDENT.
270 *
271 * @param ast the IDENT node to process
272 */
273 private void processIdent(DetailAST ast) {
274 final DetailAST parent = ast.getParent();
275 final int parentType = parent.getType();
276
277 // Ignore IDENTs that are part of the import statement itself
278 final boolean collect = parentType != TokenTypes.IMPORT
279 && parentType != TokenTypes.STATIC_IMPORT;
280
281 if (collect) {
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 /**
295 * Checks whether ast is a fully qualified identifier.
296 *
297 * @param ast to check
298 * @return true if given ast is a fully qualified identifier
299 */
300 private static boolean isQualifiedIdentifier(DetailAST ast) {
301 final DetailAST parent = ast.getParent();
302 final int parentType = parent.getType();
303
304 final boolean isQualifiedIdent = parentType == TokenTypes.DOT
305 && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
306 && ast.getNextSibling() != null;
307 final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
308 && ast.getNextSibling() != null;
309 return isQualifiedIdent || isQualifiedIdentFromMethodRef;
310 }
311
312 /**
313 * Collects the details of imports.
314 *
315 * @param ast node containing the import details
316 */
317 private void processImport(DetailAST ast) {
318 final FullIdent name = FullIdent.createFullIdentBelow(ast);
319 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
320 imports.add(name);
321 }
322 }
323
324 /**
325 * Collects the details of static imports.
326 *
327 * @param ast node containing the static import details
328 */
329 private void processStaticImport(DetailAST ast) {
330 final FullIdent name =
331 FullIdent.createFullIdent(
332 ast.getFirstChild().getNextSibling());
333 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
334 imports.add(name);
335 }
336 }
337
338 /**
339 * Processes a Javadoc reference to record referenced types.
340 *
341 * @param ast the Javadoc reference node
342 */
343 private void processReference(DetailNode ast) {
344 final String referenceText = topLevelType(ast.getFirstChild().getText());
345 currentFrame.addReferencedType(referenceText);
346 }
347
348 /**
349 * Processes a Javadoc parameter type tag to record referenced type.
350 *
351 * @param ast the Javadoc parameter type node
352 */
353 private void processParameterType(DetailNode ast) {
354 addReferencedTypesFromType(ast.getText());
355 }
356
357 /**
358 * Registers all type names referenced in a type string.
359 * Handles generic type arguments, wildcard bounds, and array suffixes.
360 *
361 * @param type the type string to process
362 */
363 private void addReferencedTypesFromType(String type) {
364 String currentType = type;
365 if (currentType.startsWith(WILDCARD_EXTENDS_PREFIX)) {
366 currentType = currentType.substring(WILDCARD_EXTENDS_PREFIX.length());
367 }
368 else if (currentType.startsWith(WILDCARD_SUPER_PREFIX)) {
369 currentType = currentType.substring(WILDCARD_SUPER_PREFIX.length());
370 }
371 else {
372 currentType = stripTrailingParameterName(currentType);
373 }
374 if (currentType.endsWith("[]")) {
375 currentType = currentType.substring(0, currentType.length() - 2);
376 }
377 String outerType = stripTypeArguments(currentType);
378 outerType = stripTrailingGt(outerType);
379 outerType = topLevelType(outerType);
380 currentFrame.addReferencedType(outerType);
381 final int openIndex = currentType.indexOf('<');
382 if (openIndex != -1) {
383 final int closeIndex = findMatchingCloseAngle(currentType, openIndex);
384 if (closeIndex != -1) {
385 final String typeArgs = currentType.substring(openIndex + 1, closeIndex);
386 for (String arg : splitTypeArguments(typeArgs)) {
387 addReferencedTypesFromType(arg);
388 }
389 }
390 }
391 }
392
393 /**
394 * Processes a Javadoc throws or exception tag to record referenced type.
395 *
396 * @param ast the Javadoc throws or exception node
397 */
398 private void processException(DetailNode ast) {
399 final DetailNode ident =
400 JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER);
401 if (ident != null) {
402 currentFrame.addReferencedType(ident.getText());
403 }
404 }
405
406 /**
407 * If the given type string contains "." (e.g. "Map.Entry"), returns the
408 * top level type (e.g. "Map"), as that is what must be imported for the
409 * type to resolve. Otherwise, returns the type as-is.
410 *
411 * @param type A possibly qualified type name
412 * @return The simple name of the top level type
413 */
414 private static String topLevelType(String type) {
415 String result = type;
416 final int dotIndex = type.indexOf('.');
417 if (dotIndex != -1) {
418 result = type.substring(0, dotIndex);
419 }
420 return result;
421 }
422
423 /**
424 * Strips generic type arguments from a type string.
425 *
426 * @param type A type string possibly containing type arguments
427 * @return The type string with type arguments removed
428 */
429 private static String stripTypeArguments(final String type) {
430 final int index = type.indexOf('<');
431 final String result;
432 if (index == -1) {
433 result = type;
434 }
435 else {
436 result = type.substring(0, index);
437 }
438 return result;
439 }
440
441 /**
442 * Strips trailing {@code >} characters from a type string.
443 * This handles tokenization artifacts where the closing angle bracket
444 * of an enclosing generic is attached to the last parameter type.
445 *
446 * @param type A type string possibly ending with {@code >}
447 * @return The type string with trailing {@code >} characters removed
448 */
449 private static String stripTrailingGt(String type) {
450 String result = type;
451 while (result.endsWith(">")) {
452 result = result.substring(0, result.length() - 1);
453 }
454 return result;
455 }
456
457 /**
458 * Strips a trailing parameter name (e.g. "outputTarget" in
459 * "Result outputTarget") from a type token when the
460 * Javadoc lexer merges the type and parameter name into a
461 * single PARAMETER_TYPE token.
462 *
463 * <p>Only strips if the substring after the last space is a valid
464 * Java identifier, which is true for a parameter name but false
465 * for generic content such as {@code BigDecimal>} in
466 * {@code Class<? extends BigDecimal>}.</p>
467 *
468 * @param type the raw token text
469 * @return the type portion with any trailing parameter name removed
470 */
471 private static String stripTrailingParameterName(String type) {
472 final int lastSpace = type.lastIndexOf(' ');
473 String result = type;
474 if (lastSpace != -1) {
475 final String after = type.substring(lastSpace + 1);
476 if (PARAM_NAME_PATTERN.matcher(after).matches()) {
477 result = type.substring(0, lastSpace);
478 }
479 }
480 return result;
481 }
482
483 /**
484 * Finds the matching close angle bracket for the angle bracket at the given index.
485 * Correctly handles nested angle brackets by tracking depth.
486 *
487 * @param str the string to search in
488 * @param openIndex the index of the opening angle bracket
489 * @return the index of the matching close angle bracket, or -1 if not found
490 */
491 private static int findMatchingCloseAngle(String str, int openIndex) {
492 int depth = 0;
493 int result = -1;
494 for (int idx = openIndex; idx < str.length(); idx++) {
495 if (str.charAt(idx) == '<') {
496 depth++;
497 }
498 else if (str.charAt(idx) == '>') {
499 depth--;
500 if (depth == 0) {
501 result = idx;
502 break;
503 }
504 }
505 }
506 return result;
507 }
508
509 /**
510 * Splits a type argument string into individual type argument strings.
511 * Comma handling is deferred to a follow-up issue that absorbs commas into
512 * PARAMETER_TYPE; currently commas are token boundaries so only one type
513 * argument ever appears here.
514 *
515 * @param typeArgs the type argument string (content between angle brackets)
516 * @return the list of individual type argument strings
517 */
518 private static List<String> splitTypeArguments(String typeArgs) {
519 final List<String> result = new ArrayList<>();
520 result.add(typeArgs);
521 return result;
522 }
523
524 /**
525 * Holds the names of referenced types and names of declared inner types.
526 */
527 private static final class Frame {
528
529 /** Parent frame. */
530 private final Frame parent;
531
532 /** Nested types declared in the current scope. */
533 private final Set<String> declaredTypes;
534
535 /** Set of references - possibly to imports or locally declared types. */
536 private final Set<String> referencedTypes;
537
538 /**
539 * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
540 *
541 * @param parent the parent frame
542 */
543 private Frame(Frame parent) {
544 this.parent = parent;
545 declaredTypes = new HashSet<>();
546 referencedTypes = new HashSet<>();
547 }
548
549 /**
550 * Adds new inner type.
551 *
552 * @param type the type name
553 */
554 /* package */ void addDeclaredType(String type) {
555 declaredTypes.add(type);
556 }
557
558 /**
559 * Adds new type reference to the current frame.
560 *
561 * @param type the type name
562 */
563 /* package */ void addReferencedType(String type) {
564 referencedTypes.add(type);
565 }
566
567 /**
568 * Adds new inner types.
569 *
570 * @param types the type names
571 */
572 /* package */ void addReferencedTypes(Collection<String> types) {
573 referencedTypes.addAll(types);
574 }
575
576 /**
577 * Filters out all references to locally defined types.
578 *
579 */
580 /* package */ void finish() {
581 referencedTypes.removeAll(declaredTypes);
582 }
583
584 /**
585 * Creates new inner frame.
586 *
587 * @return a new frame.
588 */
589 /* package */ Frame push() {
590 return new Frame(this);
591 }
592
593 /**
594 * Pulls all referenced types up, except those that are declared in this scope.
595 *
596 * @return the parent frame
597 */
598 /* package */ Frame pop() {
599 finish();
600 parent.addReferencedTypes(referencedTypes);
601 return parent;
602 }
603
604 /**
605 * Checks whether this type name is used in this frame.
606 *
607 * @param type the type name
608 * @return {@code true} if the type is used
609 */
610 /* package */ boolean isReferencedType(String type) {
611 return referencedTypes.contains(type);
612 }
613
614 /**
615 * Creates a new top-level frame for the compilation unit.
616 *
617 * @return a new frame.
618 */
619 /* package */ static Frame compilationUnit() {
620 return new Frame(null);
621 }
622
623 }
624
625 }