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