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