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     * Setter to control whether to process Javadoc comments.
136     *
137     * @param value Flag for processing Javadoc comments.
138     * @since 5.4
139     */
140    public void setProcessJavadoc(boolean value) {
141        processJavadoc = value;
142    }
143
144    /**
145     * Setter to control when to print violations if the Javadoc being examined by this check
146     * violates the tight html rules defined at
147     * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
148     *     Tight-HTML Rules</a>.
149     *
150     * @param shouldReportViolation value to which the field shall be set to
151     * @since 8.3
152     * @propertySince 13.4.0
153     */
154    @Override
155    public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
156        super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
157    }
158
159    @Override
160    public void beginTree(DetailAST rootAST) {
161        super.beginTree(rootAST);
162        currentFrame = Frame.compilationUnit();
163        imports.clear();
164    }
165
166    @Override
167    public void finishTree(DetailAST rootAST) {
168        currentFrame.finish();
169        // loop over all the imports to see if referenced.
170        imports.stream()
171            .filter(imprt -> isUnusedImport(imprt.getText()))
172            .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
173    }
174
175    @Override
176    public int[] getRequiredJavadocTokens() {
177        return new int[] {
178            JavadocCommentsTokenTypes.REFERENCE,
179            JavadocCommentsTokenTypes.PARAMETER_TYPE,
180            JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
181            JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
182        };
183    }
184
185    @Override
186    public int[] getDefaultJavadocTokens() {
187        return getRequiredJavadocTokens();
188    }
189
190    @Override
191    public void visitJavadocToken(DetailNode ast) {
192        switch (ast.getType()) {
193            case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast);
194            case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast);
195            case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
196                 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast);
197            default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
198        }
199
200    }
201
202    @Override
203    public int[] getDefaultTokens() {
204        return getRequiredTokens();
205    }
206
207    @Override
208    public int[] getAcceptableTokens() {
209        return getRequiredTokens();
210    }
211
212    @Override
213    public int[] getRequiredTokens() {
214        return new int[] {
215            TokenTypes.IDENT,
216            TokenTypes.IMPORT,
217            TokenTypes.STATIC_IMPORT,
218            // Tokens for creating a new frame
219            TokenTypes.OBJBLOCK,
220            TokenTypes.SLIST,
221            // Javadoc
222            TokenTypes.BLOCK_COMMENT_BEGIN,
223        };
224    }
225
226    @Override
227    public void visitToken(DetailAST ast) {
228        switch (ast.getType()) {
229            case TokenTypes.IDENT -> processIdent(ast);
230            case TokenTypes.IMPORT -> processImport(ast);
231            case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
232            case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
233            case TokenTypes.BLOCK_COMMENT_BEGIN -> {
234                if (processJavadoc) {
235                    super.visitToken(ast);
236                }
237            }
238            default -> throw new IllegalArgumentException("Unknown token type " + ast);
239        }
240    }
241
242    @Override
243    public void leaveToken(DetailAST ast) {
244        if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
245            currentFrame = currentFrame.pop();
246        }
247    }
248
249    /**
250     * Checks whether an import is unused.
251     *
252     * @param imprt an import.
253     * @return true if an import is unused.
254     */
255    private boolean isUnusedImport(String imprt) {
256        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
257        return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
258            || javaLangPackageMatcher.matches();
259    }
260
261    /**
262     * Collects references made by IDENT.
263     *
264     * @param ast the IDENT node to process
265     */
266    private void processIdent(DetailAST ast) {
267        final DetailAST parent = ast.getParent();
268        final int parentType = parent.getType();
269
270        // Ignore IDENTs that are part of the import statement itself
271        final boolean collect = parentType != TokenTypes.IMPORT
272                && parentType != TokenTypes.STATIC_IMPORT;
273
274        if (collect) {
275            final boolean isClassOrMethod = parentType == TokenTypes.DOT
276                || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
277
278            if (TokenUtil.isTypeDeclaration(parentType)) {
279                currentFrame.addDeclaredType(ast.getText());
280            }
281            else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
282                currentFrame.addReferencedType(ast.getText());
283            }
284        }
285    }
286
287    /**
288     * Checks whether ast is a fully qualified identifier.
289     *
290     * @param ast to check
291     * @return true if given ast is a fully qualified identifier
292     */
293    private static boolean isQualifiedIdentifier(DetailAST ast) {
294        final DetailAST parent = ast.getParent();
295        final int parentType = parent.getType();
296
297        final boolean isQualifiedIdent = parentType == TokenTypes.DOT
298                && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
299                && ast.getNextSibling() != null;
300        final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
301                && ast.getNextSibling() != null;
302        return isQualifiedIdent || isQualifiedIdentFromMethodRef;
303    }
304
305    /**
306     * Collects the details of imports.
307     *
308     * @param ast node containing the import details
309     */
310    private void processImport(DetailAST ast) {
311        final FullIdent name = FullIdent.createFullIdentBelow(ast);
312        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
313            imports.add(name);
314        }
315    }
316
317    /**
318     * Collects the details of static imports.
319     *
320     * @param ast node containing the static import details
321     */
322    private void processStaticImport(DetailAST ast) {
323        final FullIdent name =
324            FullIdent.createFullIdent(
325                ast.getFirstChild().getNextSibling());
326        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
327            imports.add(name);
328        }
329    }
330
331    /**
332     * Processes a Javadoc reference to record referenced types.
333     *
334     * @param ast the Javadoc reference node
335     */
336    private void processReference(DetailNode ast) {
337        final String referenceText = topLevelType(ast.getFirstChild().getText());
338        currentFrame.addReferencedType(referenceText);
339    }
340
341    /**
342     * Processes a Javadoc parameter type tag to record referenced type.
343     *
344     * @param ast the Javadoc parameter type node
345     */
346    private void processParameterType(DetailNode ast) {
347        addReferencedTypesFromType(ast.getText());
348    }
349
350    /**
351     * Registers all type names referenced in a type string.
352     * Handles generic type arguments, wildcard bounds, and array suffixes.
353     *
354     * @param type the type string to process
355     */
356    private void addReferencedTypesFromType(String type) {
357        String currentType = type;
358        if (currentType.startsWith(WILDCARD_EXTENDS_PREFIX)) {
359            currentType = currentType.substring(WILDCARD_EXTENDS_PREFIX.length());
360        }
361        else if (currentType.startsWith(WILDCARD_SUPER_PREFIX)) {
362            currentType = currentType.substring(WILDCARD_SUPER_PREFIX.length());
363        }
364        else {
365            currentType = stripTrailingParameterName(currentType);
366        }
367        if (currentType.endsWith("[]")) {
368            currentType = currentType.substring(0, currentType.length() - 2);
369        }
370        String outerType = stripTypeArguments(currentType);
371        outerType = stripTrailingGt(outerType);
372        outerType = topLevelType(outerType);
373        currentFrame.addReferencedType(outerType);
374        final int openIndex = currentType.indexOf('<');
375        if (openIndex != -1) {
376            final int closeIndex = findMatchingCloseAngle(currentType, openIndex);
377            if (closeIndex != -1) {
378                final String typeArgs = currentType.substring(openIndex + 1, closeIndex);
379                for (String arg : splitTypeArguments(typeArgs)) {
380                    addReferencedTypesFromType(arg);
381                }
382            }
383        }
384    }
385
386    /**
387     * Processes a Javadoc throws or exception tag to record referenced type.
388     *
389     * @param ast the Javadoc throws or exception node
390     */
391    private void processException(DetailNode ast) {
392        final DetailNode ident =
393                JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER);
394        if (ident != null) {
395            currentFrame.addReferencedType(ident.getText());
396        }
397    }
398
399    /**
400     * If the given type string contains "." (e.g. "Map.Entry"), returns the
401     * top level type (e.g. "Map"), as that is what must be imported for the
402     * type to resolve. Otherwise, returns the type as-is.
403     *
404     * @param type A possibly qualified type name
405     * @return The simple name of the top level type
406     */
407    private static String topLevelType(String type) {
408        String result = type;
409        final int dotIndex = type.indexOf('.');
410        if (dotIndex != -1) {
411            result = type.substring(0, dotIndex);
412        }
413        return result;
414    }
415
416    /**
417     * Strips generic type arguments from a type string.
418     *
419     * @param type A type string possibly containing type arguments
420     * @return The type string with type arguments removed
421     */
422    private static String stripTypeArguments(final String type) {
423        final int index = type.indexOf('<');
424        final String result;
425        if (index == -1) {
426            result = type;
427        }
428        else {
429            result = type.substring(0, index);
430        }
431        return result;
432    }
433
434    /**
435     * Strips trailing {@code >} characters from a type string.
436     * This handles tokenization artifacts where the closing angle bracket
437     * of an enclosing generic is attached to the last parameter type.
438     *
439     * @param type A type string possibly ending with {@code >}
440     * @return The type string with trailing {@code >} characters removed
441     */
442    private static String stripTrailingGt(String type) {
443        String result = type;
444        while (result.endsWith(">")) {
445            result = result.substring(0, result.length() - 1);
446        }
447        return result;
448    }
449
450    /**
451     * Strips a trailing parameter name (e.g. &quot;outputTarget&quot; in
452     * &quot;Result outputTarget&quot;) from a type token when the
453     * Javadoc lexer merges the type and parameter name into a
454     * single PARAMETER_TYPE token.
455     *
456     * <p>Only strips if the substring after the last space is a valid
457     * Java identifier, which is true for a parameter name but false
458     * for generic content such as {@code BigDecimal>} in
459     * {@code Class<? extends BigDecimal>}.</p>
460     *
461     * @param type the raw token text
462     * @return the type portion with any trailing parameter name removed
463     */
464    private static String stripTrailingParameterName(String type) {
465        final int lastSpace = type.lastIndexOf(' ');
466        String result = type;
467        if (lastSpace != -1) {
468            final String after = type.substring(lastSpace + 1);
469            if (PARAM_NAME_PATTERN.matcher(after).matches()) {
470                result = type.substring(0, lastSpace);
471            }
472        }
473        return result;
474    }
475
476    /**
477     * Finds the matching close angle bracket for the angle bracket at the given index.
478     * Correctly handles nested angle brackets by tracking depth.
479     *
480     * @param str the string to search in
481     * @param openIndex the index of the opening angle bracket
482     * @return the index of the matching close angle bracket, or -1 if not found
483     */
484    private static int findMatchingCloseAngle(String str, int openIndex) {
485        int depth = 0;
486        int result = -1;
487        for (int idx = openIndex; idx < str.length(); idx++) {
488            if (str.charAt(idx) == '<') {
489                depth++;
490            }
491            else if (str.charAt(idx) == '>') {
492                depth--;
493                if (depth == 0) {
494                    result = idx;
495                    break;
496                }
497            }
498        }
499        return result;
500    }
501
502    /**
503     * Splits a type argument string into individual type argument strings.
504     * Comma handling is deferred to a follow-up issue that absorbs commas into
505     * PARAMETER_TYPE; currently commas are token boundaries so only one type
506     * argument ever appears here.
507     *
508     * @param typeArgs the type argument string (content between angle brackets)
509     * @return the list of individual type argument strings
510     */
511    private static List<String> splitTypeArguments(String typeArgs) {
512        final List<String> result = new ArrayList<>();
513        result.add(typeArgs);
514        return result;
515    }
516
517    /**
518     * Holds the names of referenced types and names of declared inner types.
519     */
520    private static final class Frame {
521
522        /** Parent frame. */
523        private final Frame parent;
524
525        /** Nested types declared in the current scope. */
526        private final Set<String> declaredTypes;
527
528        /** Set of references - possibly to imports or locally declared types. */
529        private final Set<String> referencedTypes;
530
531        /**
532         * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
533         *
534         * @param parent the parent frame
535         */
536        private Frame(Frame parent) {
537            this.parent = parent;
538            declaredTypes = new HashSet<>();
539            referencedTypes = new HashSet<>();
540        }
541
542        /**
543         * Adds new inner type.
544         *
545         * @param type the type name
546         */
547        /* package */ void addDeclaredType(String type) {
548            declaredTypes.add(type);
549        }
550
551        /**
552         * Adds new type reference to the current frame.
553         *
554         * @param type the type name
555         */
556        /* package */ void addReferencedType(String type) {
557            referencedTypes.add(type);
558        }
559
560        /**
561         * Adds new inner types.
562         *
563         * @param types the type names
564         */
565        /* package */ void addReferencedTypes(Collection<String> types) {
566            referencedTypes.addAll(types);
567        }
568
569        /**
570         * Filters out all references to locally defined types.
571         *
572         */
573        /* package */ void finish() {
574            referencedTypes.removeAll(declaredTypes);
575        }
576
577        /**
578         * Creates new inner frame.
579         *
580         * @return a new frame.
581         */
582        /* package */ Frame push() {
583            return new Frame(this);
584        }
585
586        /**
587         * Pulls all referenced types up, except those that are declared in this scope.
588         *
589         * @return the parent frame
590         */
591        /* package */ Frame pop() {
592            finish();
593            parent.addReferencedTypes(referencedTypes);
594            return parent;
595        }
596
597        /**
598         * Checks whether this type name is used in this frame.
599         *
600         * @param type the type name
601         * @return {@code true} if the type is used
602         */
603        /* package */ boolean isReferencedType(String type) {
604            return referencedTypes.contains(type);
605        }
606
607        /**
608         * Creates a new top-level frame for the compilation unit.
609         *
610         * @return a new frame.
611         */
612        /* package */ static Frame compilationUnit() {
613            return new Frame(null);
614        }
615
616    }
617
618}