001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.HashSet;
023import java.util.Set;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.FullIdent;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030
031/**
032 * <div>
033 * Checks for redundant import statements. An import statement is
034 * considered redundant if:
035 * </div>
036 * <ul>
037 *   <li>It is a duplicate of another import. This is, when a class is imported
038 *   more than once.</li>
039 *   <li>The class non-statically imported is from the {@code java.lang}
040 *   package, e.g. importing {@code java.lang.String}.</li>
041 *   <li>The class non-statically imported is from the same package as the
042 *   current package.</li>
043 * </ul>
044 *
045 * <p>
046 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
047 * </p>
048 *
049 * <p>
050 * Violation Message Keys:
051 * </p>
052 * <ul>
053 * <li>
054 * {@code import.duplicate}
055 * </li>
056 * <li>
057 * {@code import.lang}
058 * </li>
059 * <li>
060 * {@code import.same}
061 * </li>
062 * </ul>
063 *
064 * @since 3.0
065 */
066@FileStatefulCheck
067public class RedundantImportCheck
068    extends AbstractCheck {
069
070    /**
071     * A key is pointing to the warning message text in "messages.properties"
072     * file.
073     */
074    public static final String MSG_LANG = "import.lang";
075
076    /**
077     * A key is pointing to the warning message text in "messages.properties"
078     * file.
079     */
080    public static final String MSG_SAME = "import.same";
081
082    /**
083     * A key is pointing to the warning message text in "messages.properties"
084     * file.
085     */
086    public static final String MSG_DUPLICATE = "import.duplicate";
087
088    /** Set of the imports. */
089    private final Set<FullIdent> imports = new HashSet<>();
090    /** Set of static imports. */
091    private final Set<FullIdent> staticImports = new HashSet<>();
092
093    /** Name of package in file. */
094    private String pkgName;
095
096    @Override
097    public void beginTree(DetailAST aRootAST) {
098        pkgName = null;
099        imports.clear();
100        staticImports.clear();
101    }
102
103    @Override
104    public int[] getDefaultTokens() {
105        return getRequiredTokens();
106    }
107
108    @Override
109    public int[] getAcceptableTokens() {
110        return getRequiredTokens();
111    }
112
113    @Override
114    public int[] getRequiredTokens() {
115        return new int[] {
116            TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT, TokenTypes.PACKAGE_DEF,
117        };
118    }
119
120    @Override
121    public void visitToken(DetailAST ast) {
122        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
123            pkgName = FullIdent.createFullIdent(
124                    ast.getLastChild().getPreviousSibling()).getText();
125        }
126        else if (ast.getType() == TokenTypes.IMPORT) {
127            final FullIdent imp = FullIdent.createFullIdentBelow(ast);
128            final String importText = imp.getText();
129            if (isFromPackage(importText, "java.lang")) {
130                log(ast, MSG_LANG, importText);
131            }
132            // imports from unnamed package are not allowed,
133            // so we are checking SAME rule only for named packages
134            else if (pkgName != null && isFromPackage(importText, pkgName)) {
135                log(ast, MSG_SAME, importText);
136            }
137            // Check for a duplicate import
138            imports.stream().filter(full -> importText.equals(full.getText()))
139                .forEach(full -> log(ast, MSG_DUPLICATE, full.getLineNo(), importText));
140
141            imports.add(imp);
142        }
143        else {
144            // Check for a duplicate static import
145            final FullIdent imp =
146                FullIdent.createFullIdent(
147                    ast.getLastChild().getPreviousSibling());
148            staticImports.stream().filter(full -> imp.getText().equals(full.getText()))
149                .forEach(full -> log(ast, MSG_DUPLICATE, full.getLineNo(), imp.getText()));
150
151            staticImports.add(imp);
152        }
153    }
154
155    /**
156     * Determines if an import statement is for types from a specified package.
157     *
158     * @param importName the import name
159     * @param pkg the package name
160     * @return whether from the package
161     */
162    private static boolean isFromPackage(String importName, String pkg) {
163        // imports from unnamed package are not allowed:
164        // https://docs.oracle.com/javase/specs/jls/se7/html/jls-7.html#jls-7.5
165        // So '.' must be present in member name and we are not checking for it
166        final int index = importName.lastIndexOf('.');
167        final String front = importName.substring(0, index);
168        return pkg.equals(front);
169    }
170
171}