001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 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.annotation; 021 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024 025import org.apache.commons.lang3.ArrayUtils; 026 027import com.puppycrawl.tools.checkstyle.api.Check; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 032 033/** 034 * <p> 035 * This check allows you to specify what warnings that 036 * {@link SuppressWarnings SuppressWarnings} is not 037 * allowed to suppress. You can also specify a list 038 * of TokenTypes that the configured warning(s) cannot 039 * be suppressed on. 040 * </p> 041 * 042 * <p> 043 * The {@link #setFormat warnings} property is a 044 * regex pattern. Any warning being suppressed matching 045 * this pattern will be flagged. 046 * </p> 047 * 048 * <p> 049 * By default, any warning specified will be disallowed on 050 * all legal TokenTypes unless otherwise specified via 051 * the 052 * {@link Check#setTokens(String[]) tokens} 053 * property. 054 * 055 * Also, by default warnings that are empty strings or all 056 * whitespace (regex: ^$|^\s+$) are flagged. By specifying, 057 * the format property these defaults no longer apply. 058 * </p> 059 * 060 * <p>Limitations: This check does not consider conditionals 061 * inside the SuppressWarnings annotation. <br> 062 * For example: 063 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}. 064 * According to the above example, the "unused" warning is being suppressed 065 * not the "unchecked" or "foo" warnings. All of these warnings will be 066 * considered and matched against regardless of what the conditional 067 * evaluates to. 068 * <br> 069 * The check also does not support code like {@code @SuppressWarnings("un" + "used")}, 070 * {@code @SuppressWarnings((String) "unused")} or 071 * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}. 072 * </p> 073 * 074 * <p>This check can be configured so that the "unchecked" 075 * and "unused" warnings cannot be suppressed on 076 * anything but variable and parameter declarations. 077 * See below of an example. 078 * </p> 079 * 080 * <pre> 081 * <module name="SuppressWarnings"> 082 * <property name="format" 083 * value="^unchecked$|^unused$"/> 084 * <property name="tokens" 085 * value=" 086 * CLASS_DEF,INTERFACE_DEF,ENUM_DEF, 087 * ANNOTATION_DEF,ANNOTATION_FIELD_DEF, 088 * ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF 089 * "/> 090 * </module> 091 * </pre> 092 * @author Travis Schneeberger 093 */ 094public class SuppressWarningsCheck extends Check { 095 /** 096 * A key is pointing to the warning message text in "messages.properties" 097 * file. 098 */ 099 public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED = 100 "suppressed.warning.not.allowed"; 101 102 /** {@link SuppressWarnings SuppressWarnings} annotation name. */ 103 private static final String SUPPRESS_WARNINGS = "SuppressWarnings"; 104 105 /** 106 * Fully-qualified {@link SuppressWarnings SuppressWarnings} 107 * annotation name. 108 */ 109 private static final String FQ_SUPPRESS_WARNINGS = 110 "java.lang." + SUPPRESS_WARNINGS; 111 112 /** The format string of the regexp. */ 113 private String format = "^$|^\\s+$"; 114 115 /** The regexp to match against. */ 116 private Pattern regexp = Pattern.compile(format); 117 118 /** 119 * Set the format to the specified regular expression. 120 * @param format a {@code String} value 121 * @throws org.apache.commons.beanutils.ConversionException unable to parse format 122 */ 123 public final void setFormat(String format) { 124 this.format = format; 125 regexp = CommonUtils.createPattern(format); 126 } 127 128 @Override 129 public final int[] getDefaultTokens() { 130 return getAcceptableTokens(); 131 } 132 133 @Override 134 public final int[] getAcceptableTokens() { 135 return new int[] { 136 TokenTypes.CLASS_DEF, 137 TokenTypes.INTERFACE_DEF, 138 TokenTypes.ENUM_DEF, 139 TokenTypes.ANNOTATION_DEF, 140 TokenTypes.ANNOTATION_FIELD_DEF, 141 TokenTypes.ENUM_CONSTANT_DEF, 142 TokenTypes.PARAMETER_DEF, 143 TokenTypes.VARIABLE_DEF, 144 TokenTypes.METHOD_DEF, 145 TokenTypes.CTOR_DEF, 146 }; 147 } 148 149 @Override 150 public int[] getRequiredTokens() { 151 return ArrayUtils.EMPTY_INT_ARRAY; 152 } 153 154 @Override 155 public void visitToken(final DetailAST ast) { 156 final DetailAST annotation = getSuppressWarnings(ast); 157 158 if (annotation == null) { 159 return; 160 } 161 162 final DetailAST warningHolder = 163 findWarningsHolder(annotation); 164 165 final DetailAST token = 166 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 167 DetailAST warning; 168 169 if (token == null) { 170 warning = warningHolder.findFirstToken(TokenTypes.EXPR); 171 } 172 else { 173 // case like '@SuppressWarnings(value = UNUSED)' 174 warning = token.findFirstToken(TokenTypes.EXPR); 175 } 176 177 //rare case with empty array ex: @SuppressWarnings({}) 178 if (warning == null) { 179 //check to see if empty warnings are forbidden -- are by default 180 logMatch(warningHolder.getLineNo(), 181 warningHolder.getColumnNo(), ""); 182 return; 183 } 184 185 while (warning != null) { 186 if (warning.getType() == TokenTypes.EXPR) { 187 final DetailAST fChild = warning.getFirstChild(); 188 switch (fChild.getType()) { 189 //typical case 190 case TokenTypes.STRING_LITERAL: 191 final String warningText = 192 removeQuotes(warning.getFirstChild().getText()); 193 logMatch(warning.getLineNo(), 194 warning.getColumnNo(), warningText); 195 break; 196 // conditional case 197 // ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 198 case TokenTypes.QUESTION: 199 walkConditional(fChild); 200 break; 201 // param in constant case 202 // ex: public static final String UNCHECKED = "unchecked"; 203 // @SuppressWarnings(UNCHECKED) or @SuppressWarnings(SomeClass.UNCHECKED) 204 case TokenTypes.IDENT: 205 case TokenTypes.DOT: 206 break; 207 default: 208 // Known limitation: cases like @SuppressWarnings("un" + "used") or 209 // @SuppressWarnings((String) "unused") are not properly supported, 210 // but they should not cause exceptions. 211 } 212 } 213 warning = warning.getNextSibling(); 214 } 215 } 216 217 /** 218 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 219 * that is annotating the AST. If the annotation does not exist 220 * this method will return {@code null}. 221 * 222 * @param ast the AST 223 * @return the {@link SuppressWarnings SuppressWarnings} annotation 224 */ 225 private static DetailAST getSuppressWarnings(DetailAST ast) { 226 final DetailAST annotation = AnnotationUtility.getAnnotation( 227 ast, SUPPRESS_WARNINGS); 228 229 if (annotation == null) { 230 return AnnotationUtility.getAnnotation(ast, FQ_SUPPRESS_WARNINGS); 231 } 232 else { 233 return annotation; 234 } 235 } 236 237 /** 238 * This method looks for a warning that matches a configured expression. 239 * If found it logs a violation at the given line and column number. 240 * 241 * @param lineNo the line number 242 * @param colNum the column number 243 * @param warningText the warning. 244 */ 245 private void logMatch(final int lineNo, 246 final int colNum, final String warningText) { 247 final Matcher matcher = regexp.matcher(warningText); 248 if (matcher.matches()) { 249 log(lineNo, colNum, 250 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText); 251 } 252 } 253 254 /** 255 * Find the parent (holder) of the of the warnings (Expr). 256 * 257 * @param annotation the annotation 258 * @return a Token representing the expr. 259 */ 260 private static DetailAST findWarningsHolder(final DetailAST annotation) { 261 final DetailAST annValuePair = 262 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 263 final DetailAST annArrayInit; 264 265 if (annValuePair == null) { 266 annArrayInit = 267 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 268 } 269 else { 270 annArrayInit = 271 annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 272 } 273 274 if (annArrayInit != null) { 275 return annArrayInit; 276 } 277 278 return annotation; 279 } 280 281 /** 282 * Strips a single double quote from the front and back of a string. 283 * 284 * <p>For example: 285 * <br/> 286 * Input String = "unchecked" 287 * <br/> 288 * Output String = unchecked 289 * 290 * @param warning the warning string 291 * @return the string without two quotes 292 */ 293 private static String removeQuotes(final String warning) { 294 return warning.substring(1, warning.length() - 1); 295 } 296 297 /** 298 * Recursively walks a conditional expression checking the left 299 * and right sides, checking for matches and 300 * logging violations. 301 * 302 * @param cond a Conditional type 303 * {@link TokenTypes#QUESTION QUESTION} 304 */ 305 private void walkConditional(final DetailAST cond) { 306 if (cond.getType() != TokenTypes.QUESTION) { 307 final String warningText = 308 removeQuotes(cond.getText()); 309 logMatch(cond.getLineNo(), cond.getColumnNo(), warningText); 310 return; 311 } 312 313 walkConditional(getCondLeft(cond)); 314 walkConditional(getCondRight(cond)); 315 } 316 317 /** 318 * Retrieves the left side of a conditional. 319 * 320 * @param cond cond a conditional type 321 * {@link TokenTypes#QUESTION QUESTION} 322 * @return either the value 323 * or another conditional 324 */ 325 private static DetailAST getCondLeft(final DetailAST cond) { 326 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 327 return colon.getPreviousSibling(); 328 } 329 330 /** 331 * Retrieves the right side of a conditional. 332 * 333 * @param cond a conditional type 334 * {@link TokenTypes#QUESTION QUESTION} 335 * @return either the value 336 * or another conditional 337 */ 338 private static DetailAST getCondRight(final DetailAST cond) { 339 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 340 return colon.getNextSibling(); 341 } 342}