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