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