001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.ArrayList; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Set; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 030import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.FileContents; 033import com.puppycrawl.tools.checkstyle.api.FullIdent; 034import com.puppycrawl.tools.checkstyle.api.TextBlock; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 039 040/** 041 * <p> 042 * Checks for unused import statements. 043 * </p> 044 * <p> 045 * An example of how to configure the check is: 046 * </p> 047 * <pre> 048 * <module name="UnusedImports"/> 049 * </pre> 050 * Compatible with Java 1.5 source. 051 * 052 */ 053@FileStatefulCheck 054public class UnusedImportsCheck extends AbstractCheck { 055 056 /** 057 * A key is pointing to the warning message text in "messages.properties" 058 * file. 059 */ 060 public static final String MSG_KEY = "import.unused"; 061 062 /** Regex to match class names. */ 063 private static final Pattern CLASS_NAME = CommonUtil.createPattern( 064 "((:?[\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*)"); 065 /** Regex to match the first class name. */ 066 private static final Pattern FIRST_CLASS_NAME = CommonUtil.createPattern( 067 "^" + CLASS_NAME); 068 /** Regex to match argument names. */ 069 private static final Pattern ARGUMENT_NAME = CommonUtil.createPattern( 070 "[(,]\\s*" + CLASS_NAME.pattern()); 071 072 /** Regexp pattern to match java.lang package. */ 073 private static final Pattern JAVA_LANG_PACKAGE_PATTERN = 074 CommonUtil.createPattern("^java\\.lang\\.[a-zA-Z]+$"); 075 076 /** Suffix for the star import. */ 077 private static final String STAR_IMPORT_SUFFIX = ".*"; 078 079 /** Set of the imports. */ 080 private final Set<FullIdent> imports = new HashSet<>(); 081 082 /** Set of references - possibly to imports or other things. */ 083 private final Set<String> referenced = new HashSet<>(); 084 085 /** Flag to indicate when time to start collecting references. */ 086 private boolean collect; 087 /** Flag whether to process Javadoc comments. */ 088 private boolean processJavadoc = true; 089 090 /** 091 * Sets whether to process JavaDoc or not. 092 * 093 * @param value Flag for processing JavaDoc. 094 */ 095 public void setProcessJavadoc(boolean value) { 096 processJavadoc = value; 097 } 098 099 @Override 100 public void beginTree(DetailAST rootAST) { 101 collect = false; 102 imports.clear(); 103 referenced.clear(); 104 } 105 106 @Override 107 public void finishTree(DetailAST rootAST) { 108 // loop over all the imports to see if referenced. 109 imports.stream() 110 .filter(imprt -> isUnusedImport(imprt.getText())) 111 .forEach(imprt -> log(imprt.getDetailAst(), 112 MSG_KEY, imprt.getText())); 113 } 114 115 @Override 116 public int[] getDefaultTokens() { 117 return getRequiredTokens(); 118 } 119 120 @Override 121 public int[] getRequiredTokens() { 122 return new int[] { 123 TokenTypes.IDENT, 124 TokenTypes.IMPORT, 125 TokenTypes.STATIC_IMPORT, 126 // Definitions that may contain Javadoc... 127 TokenTypes.PACKAGE_DEF, 128 TokenTypes.ANNOTATION_DEF, 129 TokenTypes.ANNOTATION_FIELD_DEF, 130 TokenTypes.ENUM_DEF, 131 TokenTypes.ENUM_CONSTANT_DEF, 132 TokenTypes.CLASS_DEF, 133 TokenTypes.INTERFACE_DEF, 134 TokenTypes.METHOD_DEF, 135 TokenTypes.CTOR_DEF, 136 TokenTypes.VARIABLE_DEF, 137 }; 138 } 139 140 @Override 141 public int[] getAcceptableTokens() { 142 return getRequiredTokens(); 143 } 144 145 @Override 146 public void visitToken(DetailAST ast) { 147 if (ast.getType() == TokenTypes.IDENT) { 148 if (collect) { 149 processIdent(ast); 150 } 151 } 152 else if (ast.getType() == TokenTypes.IMPORT) { 153 processImport(ast); 154 } 155 else if (ast.getType() == TokenTypes.STATIC_IMPORT) { 156 processStaticImport(ast); 157 } 158 else { 159 collect = true; 160 if (processJavadoc) { 161 collectReferencesFromJavadoc(ast); 162 } 163 } 164 } 165 166 /** 167 * Checks whether an import is unused. 168 * @param imprt an import. 169 * @return true if an import is unused. 170 */ 171 private boolean isUnusedImport(String imprt) { 172 final Matcher javaLangPackageMatcher = JAVA_LANG_PACKAGE_PATTERN.matcher(imprt); 173 return !referenced.contains(CommonUtil.baseClassName(imprt)) 174 || javaLangPackageMatcher.matches(); 175 } 176 177 /** 178 * Collects references made by IDENT. 179 * @param ast the IDENT node to process 180 */ 181 private void processIdent(DetailAST ast) { 182 final DetailAST parent = ast.getParent(); 183 final int parentType = parent.getType(); 184 if (parentType != TokenTypes.DOT 185 && parentType != TokenTypes.METHOD_DEF 186 || parentType == TokenTypes.DOT 187 && ast.getNextSibling() != null) { 188 referenced.add(ast.getText()); 189 } 190 } 191 192 /** 193 * Collects the details of imports. 194 * @param ast node containing the import details 195 */ 196 private void processImport(DetailAST ast) { 197 final FullIdent name = FullIdent.createFullIdentBelow(ast); 198 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 199 imports.add(name); 200 } 201 } 202 203 /** 204 * Collects the details of static imports. 205 * @param ast node containing the static import details 206 */ 207 private void processStaticImport(DetailAST ast) { 208 final FullIdent name = 209 FullIdent.createFullIdent( 210 ast.getFirstChild().getNextSibling()); 211 if (!name.getText().endsWith(STAR_IMPORT_SUFFIX)) { 212 imports.add(name); 213 } 214 } 215 216 /** 217 * Collects references made in Javadoc comments. 218 * @param ast node to inspect for Javadoc 219 */ 220 private void collectReferencesFromJavadoc(DetailAST ast) { 221 final FileContents contents = getFileContents(); 222 final int lineNo = ast.getLineNo(); 223 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 224 if (textBlock != null) { 225 referenced.addAll(collectReferencesFromJavadoc(textBlock)); 226 } 227 } 228 229 /** 230 * Process a javadoc {@link TextBlock} and return the set of classes 231 * referenced within. 232 * @param textBlock The javadoc block to parse 233 * @return a set of classes referenced in the javadoc block 234 */ 235 private static Set<String> collectReferencesFromJavadoc(TextBlock textBlock) { 236 final List<JavadocTag> tags = new ArrayList<>(); 237 // gather all the inline tags, like @link 238 // INLINE tags inside BLOCKs get hidden when using ALL 239 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.INLINE)); 240 // gather all the block-level tags, like @throws and @see 241 tags.addAll(getValidTags(textBlock, JavadocUtil.JavadocTagType.BLOCK)); 242 243 final Set<String> references = new HashSet<>(); 244 245 tags.stream() 246 .filter(JavadocTag::canReferenceImports) 247 .forEach(tag -> references.addAll(processJavadocTag(tag))); 248 return references; 249 } 250 251 /** 252 * Returns the list of valid tags found in a javadoc {@link TextBlock}. 253 * @param cmt The javadoc block to parse 254 * @param tagType The type of tags we're interested in 255 * @return the list of tags 256 */ 257 private static List<JavadocTag> getValidTags(TextBlock cmt, 258 JavadocUtil.JavadocTagType tagType) { 259 return JavadocUtil.getJavadocTags(cmt, tagType).getValidTags(); 260 } 261 262 /** 263 * Returns a list of references found in a javadoc {@link JavadocTag}. 264 * @param tag The javadoc tag to parse 265 * @return A list of references found in this tag 266 */ 267 private static Set<String> processJavadocTag(JavadocTag tag) { 268 final Set<String> references = new HashSet<>(); 269 final String identifier = tag.getFirstArg().trim(); 270 for (Pattern pattern : new Pattern[] 271 {FIRST_CLASS_NAME, ARGUMENT_NAME}) { 272 references.addAll(matchPattern(identifier, pattern)); 273 } 274 return references; 275 } 276 277 /** 278 * Extracts a list of texts matching a {@link Pattern} from a 279 * {@link String}. 280 * @param identifier The String to match the pattern against 281 * @param pattern The Pattern used to extract the texts 282 * @return A list of texts which matched the pattern 283 */ 284 private static Set<String> matchPattern(String identifier, Pattern pattern) { 285 final Set<String> references = new HashSet<>(); 286 final Matcher matcher = pattern.matcher(identifier); 287 while (matcher.find()) { 288 references.add(topLevelType(matcher.group(1))); 289 } 290 return references; 291 } 292 293 /** 294 * If the given type string contains "." (e.g. "Map.Entry"), returns the 295 * top level type (e.g. "Map"), as that is what must be imported for the 296 * type to resolve. Otherwise, returns the type as-is. 297 * @param type A possibly qualified type name 298 * @return The simple name of the top level type 299 */ 300 private static String topLevelType(String type) { 301 final String topLevelType; 302 final int dotIndex = type.indexOf('.'); 303 if (dotIndex == -1) { 304 topLevelType = type; 305 } 306 else { 307 topLevelType = type.substring(0, dotIndex); 308 } 309 return topLevelType; 310 } 311 312}