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.Set;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.DetailNode;
031import com.puppycrawl.tools.checkstyle.api.FullIdent;
032import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
033import com.puppycrawl.tools.checkstyle.api.TokenTypes;
034import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
035import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
036import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
037import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
038
039/**
040 * <div>
041 * Checks for unused import statements. An import statement
042 * is considered unused if:
043 * </div>
044 *
045 * <ul>
046 * <li>
047 * It is not referenced in the file. The algorithm does not support wild-card
048 * imports like {@code import java.io.*;}. Most IDE's provide very sophisticated
049 * checks for imports that handle wild-card imports.
050 * </li>
051 * <li>
052 * The class imported is from the {@code java.lang} package. For example
053 * importing {@code java.lang.String}.
054 * </li>
055 * <li>
056 * The class imported is from the same package.
057 * </li>
058 * <li>
059 * A static method is imported when used as method reference. In that case,
060 * only the type needs to be imported and that's enough to resolve the method.
061 * </li>
062 * <li>
063 * <b>Optionally:</b> it is referenced in Javadoc comments. This check is on by
064 * default, but it is considered bad practice to introduce a compile-time
065 * dependency for documentation purposes only. As an example, the import
066 * {@code java.util.Set} would be considered referenced with the Javadoc
067 * comment {@code {@link Set}}. The alternative to avoid introducing a compile-time
068 * dependency would be to write the Javadoc comment as {@code {@link Set}}.
069 * </li>
070 * </ul>
071 *
072 * <p>
073 * The main limitation of this check is handling the cases where:
074 * </p>
075 * <ul>
076 * <li>
077 * An imported type has the same name as a declaration, such as a member variable.
078 * </li>
079 * <li>
080 * There are two or more static imports with the same method name
081 * (javac can distinguish imports with same name but different parameters, but checkstyle can not
082 * due to <a href="https://checkstyle.org/writingchecks.html#Limitations">limitation.</a>)
083 * </li>
084 * <li>
085 * Module import declarations are used. Checkstyle does not resolve modules and therefore cannot
086 * determine which packages or types are brought into scope by an {@code import module} declaration.
087 * See <a href="https://checkstyle.org/writingchecks.html#Limitations">limitations.</a>
088 * </li>
089 * </ul>
090 *
091 * @since 3.0
092 */
093@FileStatefulCheck
094@SuppressWarnings("UnrecognisedJavadocTag")
095public class UnusedImportsCheck extends AbstractJavadocCheck {
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    /** Regexp pattern to match java.lang package. */
104    private static final Pattern JAVA_LANG_PACKAGE_PATTERN =
105        CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$");
106
107    /** Suffix for the star import. */
108    private static final String STAR_IMPORT_SUFFIX = ".*";
109
110    /** Set of the imports. */
111    private final Set<FullIdent> imports = new HashSet<>();
112
113    /** Control whether to process Javadoc comments. */
114    private boolean processJavadoc = true;
115
116    /**
117     * The scope is being processed.
118     * Types declared in a scope can shadow imported types.
119     */
120    private Frame currentFrame;
121
122    /**
123     * Setter to control whether to process Javadoc comments.
124     *
125     * @param value Flag for processing Javadoc comments.
126     * @since 5.4
127     */
128    public void setProcessJavadoc(boolean value) {
129        processJavadoc = value;
130    }
131
132    /**
133     * Setter to control when to print violations if the Javadoc being examined by this check
134     * violates the tight html rules defined at
135     * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
136     *     Tight-HTML Rules</a>.
137     *
138     * @param shouldReportViolation value to which the field shall be set to
139     * @since 8.3
140     * @propertySince 13.4.0
141     */
142    @Override
143    public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
144        super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
145    }
146
147    @Override
148    public void beginTree(DetailAST rootAST) {
149        super.beginTree(rootAST);
150        currentFrame = Frame.compilationUnit();
151        imports.clear();
152    }
153
154    @Override
155    public void finishTree(DetailAST rootAST) {
156        currentFrame.finish();
157        // loop over all the imports to see if referenced.
158        imports.stream()
159            .filter(imprt -> isUnusedImport(imprt.getText()))
160            .forEach(imprt -> log(imprt.getDetailAst(), MSG_KEY, imprt.getText()));
161    }
162
163    @Override
164    public int[] getRequiredJavadocTokens() {
165        return new int[] {
166            JavadocCommentsTokenTypes.REFERENCE,
167            JavadocCommentsTokenTypes.PARAMETER_TYPE,
168            JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
169            JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
170        };
171    }
172
173    @Override
174    public int[] getDefaultJavadocTokens() {
175        return getRequiredJavadocTokens();
176    }
177
178    @Override
179    public void visitJavadocToken(DetailNode ast) {
180        switch (ast.getType()) {
181            case JavadocCommentsTokenTypes.REFERENCE -> processReference(ast);
182            case JavadocCommentsTokenTypes.PARAMETER_TYPE -> processParameterType(ast);
183            case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
184                 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> processException(ast);
185            default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
186        }
187
188    }
189
190    @Override
191    public int[] getDefaultTokens() {
192        return getRequiredTokens();
193    }
194
195    @Override
196    public int[] getAcceptableTokens() {
197        return getRequiredTokens();
198    }
199
200    @Override
201    public int[] getRequiredTokens() {
202        return new int[] {
203            TokenTypes.IDENT,
204            TokenTypes.IMPORT,
205            TokenTypes.STATIC_IMPORT,
206            // Tokens for creating a new frame
207            TokenTypes.OBJBLOCK,
208            TokenTypes.SLIST,
209            // Javadoc
210            TokenTypes.BLOCK_COMMENT_BEGIN,
211        };
212    }
213
214    @Override
215    public void visitToken(DetailAST ast) {
216        switch (ast.getType()) {
217            case TokenTypes.IDENT -> processIdent(ast);
218            case TokenTypes.IMPORT -> processImport(ast);
219            case TokenTypes.STATIC_IMPORT -> processStaticImport(ast);
220            case TokenTypes.OBJBLOCK, TokenTypes.SLIST -> currentFrame = currentFrame.push();
221            case TokenTypes.BLOCK_COMMENT_BEGIN -> {
222                if (processJavadoc) {
223                    super.visitToken(ast);
224                }
225            }
226            default -> throw new IllegalArgumentException("Unknown token type " + ast);
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        // Ignore IDENTs that are part of the import statement itself
259        final boolean collect = parentType != TokenTypes.IMPORT
260                && parentType != TokenTypes.STATIC_IMPORT;
261
262        if (collect) {
263            final boolean isClassOrMethod = parentType == TokenTypes.DOT
264                || parentType == TokenTypes.METHOD_DEF || parentType == TokenTypes.METHOD_REF;
265
266            if (TokenUtil.isTypeDeclaration(parentType)) {
267                currentFrame.addDeclaredType(ast.getText());
268            }
269            else if (!isClassOrMethod || isQualifiedIdentifier(ast)) {
270                currentFrame.addReferencedType(ast.getText());
271            }
272        }
273    }
274
275    /**
276     * Checks whether ast is a fully qualified identifier.
277     *
278     * @param ast to check
279     * @return true if given ast is a fully qualified identifier
280     */
281    private static boolean isQualifiedIdentifier(DetailAST ast) {
282        final DetailAST parent = ast.getParent();
283        final int parentType = parent.getType();
284
285        final boolean isQualifiedIdent = parentType == TokenTypes.DOT
286                && !TokenUtil.isOfType(ast.getPreviousSibling(), TokenTypes.DOT)
287                && ast.getNextSibling() != null;
288        final boolean isQualifiedIdentFromMethodRef = parentType == TokenTypes.METHOD_REF
289                && ast.getNextSibling() != null;
290        return isQualifiedIdent || isQualifiedIdentFromMethodRef;
291    }
292
293    /**
294     * Collects the details of imports.
295     *
296     * @param ast node containing the import details
297     */
298    private void processImport(DetailAST ast) {
299        final FullIdent name = FullIdent.createFullIdentBelow(ast);
300        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
301            imports.add(name);
302        }
303    }
304
305    /**
306     * Collects the details of static imports.
307     *
308     * @param ast node containing the static import details
309     */
310    private void processStaticImport(DetailAST ast) {
311        final FullIdent name =
312            FullIdent.createFullIdent(
313                ast.getFirstChild().getNextSibling());
314        if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) {
315            imports.add(name);
316        }
317    }
318
319    /**
320     * Processes a Javadoc reference to record referenced types.
321     *
322     * @param ast the Javadoc reference node
323     */
324    private void processReference(DetailNode ast) {
325        final String referenceText = topLevelType(ast.getFirstChild().getText());
326        currentFrame.addReferencedType(referenceText);
327    }
328
329    /**
330     * Processes a Javadoc parameter type tag to record referenced type.
331     *
332     * @param ast the Javadoc parameter type node
333     */
334    private void processParameterType(DetailNode ast) {
335        String parameterTypeText = topLevelType(ast.getText());
336        if (parameterTypeText.endsWith("[]")) {
337            parameterTypeText = parameterTypeText.substring(0, parameterTypeText.length() - 2);
338        }
339        currentFrame.addReferencedType(parameterTypeText);
340    }
341
342    /**
343     * Processes a Javadoc throws or exception tag to record referenced type.
344     *
345     * @param ast the Javadoc throws or exception node
346     */
347    private void processException(DetailNode ast) {
348        final DetailNode ident =
349                JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.IDENTIFIER);
350        if (ident != null) {
351            currentFrame.addReferencedType(ident.getText());
352        }
353    }
354
355    /**
356     * If the given type string contains "." (e.g. "Map.Entry"), returns the
357     * top level type (e.g. "Map"), as that is what must be imported for the
358     * type to resolve. Otherwise, returns the type as-is.
359     *
360     * @param type A possibly qualified type name
361     * @return The simple name of the top level type
362     */
363    private static String topLevelType(String type) {
364        String result = type;
365        final int dotIndex = type.indexOf('.');
366        if (dotIndex != -1) {
367            result = type.substring(0, dotIndex);
368        }
369        return result;
370    }
371
372    /**
373     * Holds the names of referenced types and names of declared inner types.
374     */
375    private static final class Frame {
376
377        /** Parent frame. */
378        private final Frame parent;
379
380        /** Nested types declared in the current scope. */
381        private final Set<String> declaredTypes;
382
383        /** Set of references - possibly to imports or locally declared types. */
384        private final Set<String> referencedTypes;
385
386        /**
387         * Private constructor. Use {@link #compilationUnit()} to create a new top-level frame.
388         *
389         * @param parent the parent frame
390         */
391        private Frame(Frame parent) {
392            this.parent = parent;
393            declaredTypes = new HashSet<>();
394            referencedTypes = new HashSet<>();
395        }
396
397        /**
398         * Adds new inner type.
399         *
400         * @param type the type name
401         */
402        /* package */ void addDeclaredType(String type) {
403            declaredTypes.add(type);
404        }
405
406        /**
407         * Adds new type reference to the current frame.
408         *
409         * @param type the type name
410         */
411        /* package */ void addReferencedType(String type) {
412            referencedTypes.add(type);
413        }
414
415        /**
416         * Adds new inner types.
417         *
418         * @param types the type names
419         */
420        /* package */ void addReferencedTypes(Collection<String> types) {
421            referencedTypes.addAll(types);
422        }
423
424        /**
425         * Filters out all references to locally defined types.
426         *
427         */
428        /* package */ void finish() {
429            referencedTypes.removeAll(declaredTypes);
430        }
431
432        /**
433         * Creates new inner frame.
434         *
435         * @return a new frame.
436         */
437        /* package */ Frame push() {
438            return new Frame(this);
439        }
440
441        /**
442         * Pulls all referenced types up, except those that are declared in this scope.
443         *
444         * @return the parent frame
445         */
446        /* package */ Frame pop() {
447            finish();
448            parent.addReferencedTypes(referencedTypes);
449            return parent;
450        }
451
452        /**
453         * Checks whether this type name is used in this frame.
454         *
455         * @param type the type name
456         * @return {@code true} if the type is used
457         */
458        /* package */ boolean isReferencedType(String type) {
459            return referencedTypes.contains(type);
460        }
461
462        /**
463         * Creates a new top-level frame for the compilation unit.
464         *
465         * @return a new frame.
466         */
467        /* package */ static Frame compilationUnit() {
468            return new Frame(null);
469        }
470
471    }
472
473}