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