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