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                break;
235            case TokenTypes.IMPORT:
236                processImport(ast);
237                break;
238            case TokenTypes.STATIC_IMPORT:
239                processStaticImport(ast);
240                break;
241            case TokenTypes.OBJBLOCK:
242            case TokenTypes.SLIST:
243                currentFrame = currentFrame.push();
244                break;
245            default:
246                collect = true;
247                if (processJavadoc) {
248                    collectReferencesFromJavadoc(ast);
249                }
250                break;
251        }
252    }
253
254    @Override
255    public void leaveToken(DetailAST ast) {
256        if (TokenUtil.isOfType(ast, TokenTypes.OBJBLOCK, TokenTypes.SLIST)) {
257            currentFrame = currentFrame.pop();
258        }
259    }
260
261    /**
262     * Checks whether an import is unused.
263     *
264     * @param imprt an import.
265     * @return true if an import is unused.
266     */
267    private boolean isUnusedImport(String imprt) {
268        final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt);
269        return !currentFrame.isReferencedType(CommonUtil.baseClassName(imprt))
270            || javaLangPackageMatcher.matches();
271    }
272
273    /**
274     * Collects references made by IDENT.
275     *
276     * @param ast the IDENT node to process
277     */
278    private void processIdent(DetailAST ast) {
279        final DetailAST parent = ast.getParent();
280        final int parentType = parent.getType();
281
282        final boolean isClassOrMethod = parentType == TokenTypes.DOT
283                || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
284
285        if (TokenUtil.isTypeDeclaration(parentType)) {
286            currentFrame.addDeclaredType(ast.getText());
287        }
288        else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
289            currentFrame.addReferencedType(ast.getText());
290        }
291    }
292
293    /**
294     * Checks whether ast is a fully qualified identifier.
295     *
296     * @param ast to check
297     * @return true if given ast is a fully qualified identifier
298     */
299    private static boolean isQualifiedIdentifier(DetailAST ast) {
300        final DetailAST parent = ast.getParent();
301        final int parentType = parent.getType();
302
303        final boolean isQualifiedIdent = parentType == TokenTypes.DOT
304                && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
305                && ast.getNextSibling() != null;
306        final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
307                && ast.getNextSibling() != null;
308        return isQualifiedIdent || isQualifiedIdentFromMethodRef;
309    }
310
311    /**
312     * Collects the details of imports.
313     *
314     * @param ast node containing the import details
315     */
316    private void processImport(DetailAST ast) {
317        final FullIdent name = FullIdent.createFullIdentBelow(ast);
318        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
319            imports.add(name);
320        }
321    }
322
323    /**
324     * Collects the details of static imports.
325     *
326     * @param ast node containing the static import details
327     */
328    private void processStaticImport(DetailAST ast) {
329        final FullIdent name =
330            FullIdent.createFullIdent(
331                ast.getFirstChild().getNextSibling());
332        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
333            imports.add(name);
334        }
335    }
336
337    /**
338     * Collects references made in Javadoc comments.
339     *
340     * @param ast node to inspect for Javadoc
341     */
342    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
343    @SuppressWarnings("deprecation")
344    private void collectReferencesFromJavadoc(DetailAST ast) {
345        final FileContents contents = getFileContents();
346        final int lineNo = ast.getLineNo();
347        final TextBlock textBlock = contents.getJavadocBefore(lineNo);
348        if (textBlock != null) {
349            currentFrame.addReferencedTypes(collectReferencesFromJavadoc(textBlock));
350        }
351    }
352
353    /**
354     * Process a javadoc {@link TextBlock} and return the set of classes
355     * referenced within.
356     *
357     * @param textBlock The javadoc block to parse
358     * @return a set of classes referenced in the javadoc block
359     */
360    private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) {
361        // Process INLINE tags
362        final List<JavadocTag> inlineTags = getTargetTags(textBlock,
363                JavadocUtil.JavadocTagType.INLINE);
364        // Process BLOCK tags
365        final List<JavadocTag> blockTags = getTargetTags(textBlock,
366                JavadocUtil.JavadocTagType.BLOCK);
367        final List<JavadocTag> targetTags = Stream.concat(inlineTags.stream(), blockTags.stream())
368                .toList();
369
370        final Set<String> references = new HashSet<>();
371
372        targetTags.stream()
373            .filter(JavadocTag::canReferenceImports)
374            .forEach(tag -> references.addAll(processJavadocTag(tag)));
375        return references;
376    }
377
378    /**
379     * Returns the list of valid tags found in a javadoc {@link TextBlock}.
380     * Filters tags based on whether they are inline or block tags, ensuring they match
381     * the correct format supported.
382     *
383     * @param cmt The javadoc block to parse
384     * @param javadocTagType The type of tags we're interested in
385     * @return the list of tags
386     */
387    private static List<JavadocTag> getTargetTags(TextBlock cmt,
388            JavadocUtil.JavadocTagType javadocTagType) {
389        return JavadocUtil.getJavadocTags(cmt, javadocTagType)
390            .getValidTags()
391            .stream()
392            .filter(tag -> isMatchingTagType(tag, javadocTagType))
393            .map(UnusedImportsCheck::bestTryToMatchReference)
394            .flatMap(Optional::stream)
395            .toList();
396    }
397
398    /**
399     * Returns a list of references that found in a javadoc {@link JavadocTag}.
400     *
401     * @param tag The javadoc tag to parse
402     * @return A list of references that found in this tag
403     */
404    private static Set<String> processJavadocTag(JavadocTag tag) {
405        final Set<String> references = new HashSet<>();
406        final String identifier = tag.getFirstArg();
407        for (Pattern pattern : new Pattern[]
408        {FIRST_CLASS_NAME, ARGUMENT_NAME}) {
409            references.addAll(matchPattern(identifier, pattern));
410        }
411        return references;
412    }
413
414    /**
415     * Extracts a set of texts matching a {@link Pattern} from a
416     * {@link String}.
417     *
418     * @param identifier The String to match the pattern against
419     * @param pattern The Pattern used to extract the texts
420     * @return A set of texts which matched the pattern
421     */
422    private static Set<String> matchPattern(String identifier, Pattern pattern) {
423        final Set<String> references = new HashSet<>();
424        final Matcher matcher = pattern.matcher(identifier);
425        while (matcher.find()) {
426            references.add(topLevelType(matcher.group(1)));
427        }
428        return references;
429    }
430
431    /**
432     * If the given type string contains "." (e.g. "Map.Entry"), returns the
433     * top level type (e.g. "Map"), as that is what must be imported for the
434     * type to resolve. Otherwise, returns the type as-is.
435     *
436     * @param type A possibly qualified type name
437     * @return The simple name of the top level type
438     */
439    private static String topLevelType(String type) {
440        final String topLevelType;
441        final int dotIndex = type.indexOf('.');
442        if (dotIndex == -1) {
443            topLevelType = type;
444        }
445        else {
446            topLevelType = type.substring(0, dotIndex);
447        }
448        return topLevelType;
449    }
450
451    /**
452     * Checks if a Javadoc tag matches the expected type based on its extraction format.
453     * This method checks if an inline tag is extracted as a block tag or vice versa.
454     * It ensures that block tags are correctly recognized as block tags and inline tags
455     * as inline tags during processing.
456     *
457     * @param tag The Javadoc tag to check.
458     * @param javadocTagType The expected type of the tag (BLOCK or INLINE).
459     * @return {@code true} if the tag matches the expected type, otherwise {@code false}.
460     */
461    private static boolean isMatchingTagType(JavadocTag tag,
462                                             JavadocUtil.JavadocTagType javadocTagType) {
463        final boolean isInlineTag = tag.isInlineTag();
464        final boolean isBlockTagType = javadocTagType == JavadocUtil.JavadocTagType.BLOCK;
465
466        return isBlockTagType != isInlineTag;
467    }
468
469    /**
470     * Attempts to match a reference string against a predefined pattern
471     * and extracts valid reference.
472     *
473     * @param tag the input tag to check
474     * @return Optional of extracted references
475     */
476    public static Optional<JavadocTag> bestTryToMatchReference(JavadocTag tag) {
477        final String content = tag.getFirstArg();
478        final int referenceIndex = extractReferencePart(content);
479        Optional<JavadocTag> validTag = Optional.empty();
480
481        if (referenceIndex != -1) {
482            final String referenceString;
483            if (referenceIndex == 0) {
484                referenceString = content;
485            }
486            else {
487                referenceString = content.substring(0, referenceIndex);
488            }
489            final Matcher matcher = REFERENCE.matcher(referenceString);
490            if (matcher.matches()) {
491                final int methodIndex = 3;
492                final String methodPart = matcher.group(methodIndex);
493                final boolean isValid = methodPart == null
494                        || METHOD.matcher(methodPart).matches();
495                if (isValid) {
496                    validTag = Optional.of(tag);
497                }
498            }
499        }
500        return validTag;
501    }
502
503    /**
504     * Extracts the reference part from an input string while ensuring balanced parentheses.
505     *
506     * @param input the input string
507     * @return -1 if parentheses are unbalanced, 0 if no method is found,
508     *         or the index of the first space outside parentheses.
509     */
510    private static @IndexOrLow("#1")int extractReferencePart(String input) {
511        int parenthesesCount = 0;
512        int firstSpaceOutsideParens = -1;
513        for (int index = 0; index < input.length(); index++) {
514            final char currentCharacter = input.charAt(index);
515
516            if (currentCharacter == '(') {
517                parenthesesCount++;
518            }
519            else if (currentCharacter == ')') {
520                parenthesesCount--;
521            }
522            else if (currentCharacter == ' ' && parenthesesCount == 0) {
523                firstSpaceOutsideParens = index;
524                break;
525            }
526        }
527
528        int methodIndex = -1;
529        if (parenthesesCount == 0) {
530            if (firstSpaceOutsideParens == -1) {
531                methodIndex = 0;
532            }
533            else {
534                methodIndex = firstSpaceOutsideParens;
535            }
536        }
537        return methodIndex;
538    }
539
540    /**
541     * Holds the names of referenced types and names of declared inner types.
542     */
543    private static final class Frame {
544
545        /** Parent frame. */
546        private final Frame parent;
547
548        /** Nested types declared in the current scope. */
549        private final Set<String> declaredTypes;
550
551        /** Set of references - possibly to imports or locally declared types. */
552        private final Set<String> referencedTypes;
553
554        /**
555         * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
556         *
557         * @param parent the parent frame
558         */
559        private Frame(Frame parent) {
560            this.parent = parent;
561            declaredTypes = new HashSet<>();
562            referencedTypes = new HashSet<>();
563        }
564
565        /**
566         * Adds new inner type.
567         *
568         * @param type the type name
569         */
570        public void addDeclaredType(String type) {
571            declaredTypes.add(type);
572        }
573
574        /**
575         * Adds new type reference to the current frame.
576         *
577         * @param type the type name
578         */
579        public void addReferencedType(String type) {
580            referencedTypes.add(type);
581        }
582
583        /**
584         * Adds new inner types.
585         *
586         * @param types the type names
587         */
588        public void addReferencedTypes(Collection<String> types) {
589            referencedTypes.addAll(types);
590        }
591
592        /**
593         * Filters out all references to locally defined types.
594         *
595         */
596        public void finish() {
597            referencedTypes.removeAll(declaredTypes);
598        }
599
600        /**
601         * Creates new inner frame.
602         *
603         * @return a new frame.
604         */
605        public Frame push() {
606            return new Frame(this);
607        }
608
609        /**
610         * Pulls all referenced types up, except those that are declared in this scope.
611         *
612         * @return the parent frame
613         */
614        public Frame pop() {
615            finish();
616            parent.addReferencedTypes(referencedTypes);
617            return parent;
618        }
619
620        /**
621         * Checks whether this type name is used in this frame.
622         *
623         * @param type the type name
624         * @return {@code true} if the type is used
625         */
626        public boolean isReferencedType(String type) {
627            return referencedTypes.contains(type);
628        }
629
630        /**
631         * Creates a new top-level frame for the compilation unit.
632         *
633         * @return a new frame.
634         */
635        public static Frame compilationUnit() {
636            return new Frame(null);
637        }
638
639    }
640
641}