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.Collection;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Optional;
026import java.util.Set;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import java.util.stream.Stream;
030
031import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.DetailAST;
034import com.puppycrawl.tools.checkstyle.api.FileContents;
035import com.puppycrawl.tools.checkstyle.api.FullIdent;
036import com.puppycrawl.tools.checkstyle.api.TextBlock;
037import com.puppycrawl.tools.checkstyle.api.TokenTypes;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
040import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
041import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
042
043/**
044 * <div>
045 * Checks for unused import statements. An import statement
046 * is considered unused if:
047 * </div>
048 *
049 * <ul>
050 * <li>
051 * It is not referenced in the file. The algorithm does not support wild-card
052 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
053 * checks for imports that handle wild-card imports.
054 * </li>
055 * <li>
056 * The class imported is from the {@code java.lang} package. For example
057 * importing {@code java.lang.String}.
058 * </li>
059 * <li>
060 * The class imported is from the same package.
061 * </li>
062 * <li>
063 * A static method is imported when used as method reference. In that case,
064 * only the type needs to be imported and that's enough to resolve the method.
065 * </li>
066 * <li>
067 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
068 * default, but it is considered bad practice to introduce a compile-time
069 * dependency for documentation purposes only. As an example, the import
070 * {@code java.util.List} would be considered referenced with the Javadoc
071 * comment {@code {@link List}}. The alternative to avoid introducing a compile-time
072 * dependency would be to write the Javadoc comment as {@code {&#64;link java.util.List}}.
073 * </li>
074 * </ul>
075 *
076 * <p>
077 * The main limitation of this check is handling the cases where:
078 * </p>
079 * <ul>
080 * <li>
081 * An imported type has the same name as a declaration, such as a member variable.
082 * </li>
083 * <li>
084 * There are two or more static imports with the same method name
085 * (javac can distinguish imports with same name but different parameters, but checkstyle can not
086 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
087 * </li>
088 * <li>
089 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
090 * determine which packages or types are brought into scope by an {@code import module} declaration.
091 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
092 * </li>
093 * </ul>
094 *
095 * @since 3.0
096 */
097@FileStatefulCheck
098@SuppressWarnings("UnrecognisedJavadocTag")
099public class UnusedImportsCheck extends AbstractCheck {
100
101    /**
102     * A key is pointing to the warning message text in "messages.properties"
103     * file.
104     */
105    public static final String MSG_KEY = "import.unused";
106
107    /** Regex to match class names. */
108    private static final Pattern CLASS_NAME = CommonUtil.createPattern(
109           "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)");
110    /** Regex to match the first class name. */
111    private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern(
112           "^" + CLASS_NAME);
113    /** Regex to match argument names. */
114    private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern(
115           "[(,]\\s*" + CLASS_NAME.pattern());
116
117    /** Regexp pattern to match java.lang package. */
118    private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
119        CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
120
121    /** Reference pattern. */
122    private static final Pattern REFERENCE = Pattern.compile(
123            "^([a-z_$][a-z\\d_$<>.]*)?(#(.*))?$",
124            Pattern.CASE_INSENSITIVE
125    );
126
127    /** Method pattern. */
128    private static final Pattern METHOD = Pattern.compile(
129            "^([a-z_$#][a-z\\d_$]*)(\\([^)]*\\))?$",
130            Pattern.CASE_INSENSITIVE
131    );
132
133    /** Suffix for the star import. */
134    private static final String STAR_IMPORT_SUFFIX = ".*";
135
136    /** Set of the imports. */
137    private final Set<FullIdent> imports = new HashSet<>();
138
139    /** Flag to indicate when time to start collecting references. */
140    private boolean collect;
141    /** Control whether to process Javadoc comments. */
142    private boolean processJavadoc = true;
143
144    /**
145     * The scope is being processed.
146     * Types declared in a scope can shadow imported types.
147     */
148    private Frame currentFrame;
149
150    /**
151     * Setter to control whether to process Javadoc comments.
152     *
153     * @param value Flag for processing Javadoc comments.
154     * @since 5.4
155     */
156    public void setProcessJavadoc(boolean value) {
157        processJavadoc = value;
158    }
159
160    @Override
161    public void beginTree(DetailAST rootAST) {
162        collect = false;
163        currentFrame = Frame.compilationUnit();
164        imports.clear();
165    }
166
167    @Override
168    public void finishTree(DetailAST rootAST) {
169        currentFrame.finish();
170        // loop over all the imports to see if referenced.
171        imports.stream()
172            .filter(imprt -> isUnusedImport(imprt.getText()))
173            .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
174    }
175
176    @Override
177    public int[] getDefaultTokens() {
178        return getRequiredTokens();
179    }
180
181    @Override
182    public int[] getRequiredTokens() {
183        return new int[] {
184            TokenTypes.IDENT,
185            TokenTypes.IMPORT,
186            TokenTypes.STATIC_IMPORT,
187            // Definitions that may contain Javadoc...
188            TokenTypes.PACKAGE_DEF,
189            TokenTypes.ANNOTATION_DEF,
190            TokenTypes.ANNOTATION_FIELD_DEF,
191            TokenTypes.ENUM_DEF,
192            TokenTypes.ENUM_CONSTANT_DEF,
193            TokenTypes.CLASS_DEF,
194            TokenTypes.INTERFACE_DEF,
195            TokenTypes.METHOD_DEF,
196            TokenTypes.CTOR_DEF,
197            TokenTypes.VARIABLE_DEF,
198            TokenTypes.RECORD_DEF,
199            TokenTypes.COMPACT_CTOR_DEF,
200            // Tokens for creating a new frame
201            TokenTypes.OBJBLOCK,
202            TokenTypes.SLIST,
203        };
204    }
205
206    @Override
207    public int[] getAcceptableTokens() {
208        return getRequiredTokens();
209    }
210
211    @Override
212    public void visitToken(DetailAST ast) {
213        switch (ast.getType()) {
214            case TokenTypes.IDENT -> {
215                if (collect) {
216                    processIdent(ast);
217                }
218            }
219            case TokenTypes.IMPORT -> processImport(ast);
220            case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
221            case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
222            default -> {
223                collect = true;
224                if (processJavadoc) {
225                    collectReferencesFromJavadoc(ast);
226                }
227            }
228        }
229    }
230
231    @Override
232    public void leaveToken(DetailAST ast) {
233        if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
234            currentFrame = currentFrame.pop();
235        }
236    }
237
238    /**
239     * Checks whether an import is unused.
240     *
241     * @param imprt an import.
242     * @return true if an import is unused.
243     */
244    private boolean isUnusedImport(String imprt) {
245        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
246        return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
247            || javaLangPackageMatcher.matches();
248    }
249
250    /**
251     * Collects references made by IDENT.
252     *
253     * @param ast the IDENT node to process
254     */
255    private void processIdent(DetailAST ast) {
256        final DetailAST parent = ast.getParent();
257        final int parentType = parent.getType();
258
259        final boolean isClassOrMethod = parentType == TokenTypes.DOT
260                || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
261
262        if (TokenUtil.isTypeDeclaration(parentType)) {
263            currentFrame.addDeclaredType(ast.getText());
264        }
265        else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
266            currentFrame.addReferencedType(ast.getText());
267        }
268    }
269
270    /**
271     * Checks whether ast is a fully qualified identifier.
272     *
273     * @param ast to check
274     * @return true if given ast is a fully qualified identifier
275     */
276    private static boolean isQualifiedIdentifier(DetailAST ast) {
277        final DetailAST parent = ast.getParent();
278        final int parentType = parent.getType();
279
280        final boolean isQualifiedIdent = parentType == TokenTypes.DOT
281                && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
282                && ast.getNextSibling() != null;
283        final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
284                && ast.getNextSibling() != null;
285        return isQualifiedIdent || isQualifiedIdentFromMethodRef;
286    }
287
288    /**
289     * Collects the details of imports.
290     *
291     * @param ast node containing the import details
292     */
293    private void processImport(DetailAST ast) {
294        final FullIdent name = FullIdent.createFullIdentBelow(ast);
295        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
296            imports.add(name);
297        }
298    }
299
300    /**
301     * Collects the details of static imports.
302     *
303     * @param ast node containing the static import details
304     */
305    private void processStaticImport(DetailAST ast) {
306        final FullIdent name =
307            FullIdent.createFullIdent(
308                ast.getFirstChild().getNextSibling());
309        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
310            imports.add(name);
311        }
312    }
313
314    /**
315     * Collects references made in Javadoc comments.
316     *
317     * @param ast node to inspect for Javadoc
318     */
319    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
320    @SuppressWarnings("deprecation")
321    private void collectReferencesFromJavadoc(DetailAST ast) {
322        final FileContents contents = getFileContents();
323        final int lineNo = ast.getLineNo();
324        final TextBlock textBlock = contents.getJavadocBefore(lineNo);
325        if (textBlock != null) {
326            currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
327        }
328    }
329
330    /**
331     * Process a javadoc {@link TextBlock} and return the set of classes
332     * referenced within.
333     *
334     * @param textBlock The javadoc block to parse
335     * @return a set of classes referenced in the javadoc block
336     */
337    private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
338        // Process INLINE tags
339        final List<JavadocTag> inlineTags = getTargetTags(textBlock,
340                JavadocUtil.JavadocTagType.INLINE);
341        // Process BLOCK tags
342        final List<JavadocTag> blockTags = getTargetTags(textBlock,
343                JavadocUtil.JavadocTagType.BLOCK);
344        final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream())
345                .toList();
346
347        final Set<String> references = new HashSet<>();
348
349        targetTags.stream()
350            .filter(JavadocTag::canReferenceImports)
351            .forEach(tag -> references.addAll(processJavadocTag(tag)));
352        return references;
353    }
354
355    /**
356     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
357     * Filters tags based on whether they are inline or block tags, ensuring they match
358     * the correct format supported.
359     *
360     * @param cmt The javadoc block to parse
361     * @param javadocTagType The type of tags we're interested in
362     * @return the list of tags
363     */
364    private static List<JavadocTag> getTargetTags(TextBlock cmt,
365            JavadocUtil.JavadocTagType javadocTagType) {
366        return JavadocUtil.getJavadocTags(cmt, javadocTagType)
367            .getValidTags()
368            .stream()
369            .filter(tag -> isMatchingTagType(tag, javadocTagType))
370            .map(UnusedImportsCheck::bestTryToMatchReference)
371            .flatMap(Optional::stream)
372            .toList();
373    }
374
375    /**
376     * Returns a list of references that found in a javadoc {@link JavadocTag}.
377     *
378     * @param tag The javadoc tag to parse
379     * @return A list of references that found in this tag
380     */
381    private static Set<String> processJavadocTag(JavadocTag tag) {
382        final Set<String> references = new HashSet<>();
383        final String identifier = tag.getFirstArg();
384        for (Pattern pattern : new Pattern[]
385        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
386            references.addAll(matchPattern(identifier, pattern));
387        }
388        return references;
389    }
390
391    /**
392     * Extracts a set of texts matching a {@link Pattern} from a
393     * {@link String}.
394     *
395     * @param identifier The String to match the pattern against
396     * @param pattern The Pattern used to extract the texts
397     * @return A set of texts which matched the pattern
398     */
399    private static Set<String> matchPattern(String identifier, Pattern pattern) {
400        final Set<String> references = new HashSet<>();
401        final Matcher matcher = pattern.matcher(identifier);
402        while (matcher.find()) {
403            references.add(topLevelType(matcher.group(1)));
404        }
405        return references;
406    }
407
408    /**
409     * If the given type string contains "." (e.g. "Map.Entry"), returns the
410     * top level type (e.g. "Map"), as that is what must be imported for the
411     * type to resolve. Otherwise, returns the type as-is.
412     *
413     * @param type A possibly qualified type name
414     * @return The simple name of the top level type
415     */
416    private static String topLevelType(String type) {
417        final String topLevelType;
418        final int dotIndex = type.indexOf('.');
419        if (dotIndex == -1) {
420            topLevelType = type;
421        }
422        else {
423            topLevelType = type.substring(0, dotIndex);
424        }
425        return topLevelType;
426    }
427
428    /**
429     * Checks if a Javadoc tag matches the expected type based on its extraction format.
430     * This method checks if an inline tag is extracted as a block tag or vice versa.
431     * It ensures that block tags are correctly recognized as block tags and inline tags
432     * as inline tags during processing.
433     *
434     * @param tag The Javadoc tag to check.
435     * @param javadocTagType The expected type of the tag (BLOCK or INLINE).
436     * @return {@code true} if the tag matches the expected type, otherwise {@code false}.
437     */
438    private static boolean isMatchingTagType(JavadocTag tag,
439                                             JavadocUtil.JavadocTagType javadocTagType) {
440        final boolean isInlineTag = tag.isInlineTag();
441        final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK;
442
443        return isBlockTagType != isInlineTag;
444    }
445
446    /**
447     * Attempts to match a reference string against a predefined pattern
448     * and extracts valid reference.
449     *
450     * @param tag the input tag to check
451     * @return Optional of extracted references
452     */
453    public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) {
454        final String content = tag.getFirstArg();
455        final int referenceIndex = extractReferencePart(content);
456        Optional<JavadocTag> validTag = Optional.empty();
457
458        if (referenceIndex != -1) {
459            final String referenceString;
460            if (referenceIndex == 0) {
461                referenceString = content;
462            }
463            else {
464                referenceString = content.substring(0, referenceIndex);
465            }
466            final Matcher matcher = REFERENCE.matcher(referenceString);
467            if (matcher.matches()) {
468                final int methodIndex = 3;
469                final String methodPart = matcher.group(methodIndex);
470                final boolean isValid = methodPart == null
471                        || METHOD.matcher(methodPart).matches();
472                if (isValid) {
473                    validTag = Optional.of(tag);
474                }
475            }
476        }
477        return validTag;
478    }
479
480    /**
481     * Extracts the reference part from an input string while ensuring balanced parentheses.
482     *
483     * @param input the input string
484     * @return -1 if parentheses are unbalanced, 0 if no method is found,
485     *         or the index of the first space outside parentheses.
486     */
487    private static int extractReferencePart(String input) {
488        int parenthesesCount = 0;
489        int firstSpaceOutsideParens = -1;
490        for (int index = 0; index < input.length(); index++) {
491            final char currentCharacter = input.charAt(index);
492
493            if (currentCharacter == '(') {
494                parenthesesCount++;
495            }
496            else if (currentCharacter == ')') {
497                parenthesesCount--;
498            }
499            else if (currentCharacter == ' ' && parenthesesCount == 0) {
500                firstSpaceOutsideParens = index;
501                break;
502            }
503        }
504
505        int methodIndex = -1;
506        if (parenthesesCount == 0) {
507            if (firstSpaceOutsideParens == -1) {
508                methodIndex = 0;
509            }
510            else {
511                methodIndex = firstSpaceOutsideParens;
512            }
513        }
514        return methodIndex;
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}