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/writingchecks.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/writingchecks.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"
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. &quot;outputTarget&quot; in
459     * &quot;Result outputTarget&quot;) 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}