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.Collection;
23 import java.util.HashSet;
24 import java.util.Set;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27
28 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29 import com.puppycrawl.tools.checkstyle.api.DetailAST;
30 import com.puppycrawl.tools.checkstyle.api.DetailNode;
31 import com.puppycrawl.tools.checkstyle.api.FullIdent;
32 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
33 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34 import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
35 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
36 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
37 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
38
39 /**
40 * <div>
41 * Checks for unused import statements. An import statement
42 * is considered unused if:
43 * </div>
44 *
45 * <ul>
46 * <li>
47 * It is not referenced in the file. The algorithm does not support wild-card
48 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
49 * checks for imports that handle wild-card imports.
50 * </li>
51 * <li>
52 * The class imported is from the {@code java.lang} package. For example
53 * importing {@code java.lang.String}.
54 * </li>
55 * <li>
56 * The class imported is from the same package.
57 * </li>
58 * <li>
59 * A static method is imported when used as method reference. In that case,
60 * only the type needs to be imported and that's enough to resolve the method.
61 * </li>
62 * <li>
63 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
64 * default, but it is considered bad practice to introduce a compile-time
65 * dependency for documentation purposes only. As an example, the import
66 * {@code java.util.Set} would be considered referenced with the Javadoc
67 * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time
68 * dependency would be to write the Javadoc comment as {@code {@link Set}}.
69 * </li>
70 * </ul>
71 *
72 * <p>
73 * The main limitation of this check is handling the cases where:
74 * </p>
75 * <ul>
76 * <li>
77 * An imported type has the same name as a declaration, such as a member variable.
78 * </li>
79 * <li>
80 * There are two or more static imports with the same method name
81 * (javac can distinguish imports with same name but different parameters, but checkstyle can not
82 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
83 * </li>
84 * <li>
85 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
86 * determine which packages or types are brought into scope by an {@code import module} declaration.
87 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
88 * </li>
89 * </ul>
90 *
91 * @since 3.0
92 */
93 @FileStatefulCheck
94 @SuppressWarnings("UnrecognisedJavadocTag")
95 public class UnusedImportsCheck extends AbstractJavadocCheck {
96
97 /**
98 * A key is pointing to the warning message text in "messages.properties"
99 * 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 }