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