1 //////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code for adherence to a set of rules. 3 // Copyright (C) 2001-2019 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 //////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.annotation; 21 22 import java.util.regex.Matcher; 23 import java.util.regex.Pattern; 24 25 import com.puppycrawl.tools.checkstyle.StatelessCheck; 26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 27 import com.puppycrawl.tools.checkstyle.api.DetailAST; 28 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 29 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 31 32 /** 33 * <p> 34 * This check allows you to specify what warnings that 35 * @SuppressWarnings is not allowed to suppress. 36 * You can also specify a list of TokenTypes that 37 * the configured warning(s) cannot be suppressed on. 38 * </p> 39 * <p> 40 * Limitations: This check does not consider conditionals 41 * inside the @SuppressWarnings annotation. 42 * </p> 43 * <p> 44 * For example: 45 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}. 46 * According to the above example, the "unused" warning is being suppressed 47 * not the "unchecked" or "foo" warnings. All of these warnings will be 48 * considered and matched against regardless of what the conditional 49 * evaluates to. 50 * The check also does not support code like {@code @SuppressWarnings("un" + "used")}, 51 * {@code @SuppressWarnings((String) "unused")} or 52 * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}. 53 * </p> 54 * <p> 55 * By default, any warning specified will be disallowed on 56 * all legal TokenTypes unless otherwise specified via 57 * the tokens property. 58 * </p> 59 * <p> 60 * Also, by default warnings that are empty strings or all 61 * whitespace (regex: ^$|^\s+$) are flagged. By specifying, 62 * the format property these defaults no longer apply. 63 * </p> 64 * <p>This check can be configured so that the "unchecked" 65 * and "unused" warnings cannot be suppressed on 66 * anything but variable and parameter declarations. 67 * See below of an example. 68 * </p> 69 * <ul> 70 * <li> 71 * Property {@code format} - Specify the RegExp to match against warnings. Any warning 72 * being suppressed matching this pattern will be flagged. 73 * Default value is {@code "^\s*+$"}. 74 * </li> 75 * <li> 76 * Property {@code tokens} - tokens to check 77 * Default value is: 78 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF"> 79 * CLASS_DEF</a>, 80 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF"> 81 * INTERFACE_DEF</a>, 82 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF"> 83 * ENUM_DEF</a>, 84 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF"> 85 * ANNOTATION_DEF</a>, 86 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF"> 87 * ANNOTATION_FIELD_DEF</a>, 88 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF"> 89 * ENUM_CONSTANT_DEF</a>, 90 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF"> 91 * PARAMETER_DEF</a>, 92 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF"> 93 * VARIABLE_DEF</a>, 94 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF"> 95 * METHOD_DEF</a>, 96 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF"> 97 * CTOR_DEF</a>. 98 * </li> 99 * </ul> 100 * <p> 101 * To configure the check: 102 * </p> 103 * <pre> 104 * <module name="SuppressWarnings"/> 105 * </pre> 106 * <p> 107 * To configure the check so that the "unchecked" and "unused" 108 * warnings cannot be suppressed on anything but variable and parameter declarations. 109 * </p> 110 * <pre> 111 * <module name="SuppressWarnings"> 112 * <property name="format" 113 * value="^unchecked$|^unused$"/> 114 * <property name="tokens" 115 * value=" 116 * CLASS_DEF,INTERFACE_DEF,ENUM_DEF, 117 * ANNOTATION_DEF,ANNOTATION_FIELD_DEF, 118 * ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF 119 * "/> 120 * </module> 121 * </pre> 122 * 123 * @since 5.0 124 */ 125 @StatelessCheck 126 public class SuppressWarningsCheck extends AbstractCheck { 127 128 /** 129 * A key is pointing to the warning message text in "messages.properties" 130 * file. 131 */ 132 public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED = 133 "suppressed.warning.not.allowed"; 134 135 /** {@link SuppressWarnings SuppressWarnings} annotation name. */ 136 private static final String SUPPRESS_WARNINGS = "SuppressWarnings"; 137 138 /** 139 * Fully-qualified {@link SuppressWarnings SuppressWarnings} 140 * annotation name. 141 */ 142 private static final String FQ_SUPPRESS_WARNINGS = 143 "java.lang." + SUPPRESS_WARNINGS; 144 145 /** 146 * Specify the RegExp to match against warnings. Any warning 147 * being suppressed matching this pattern will be flagged. 148 */ 149 private Pattern format = Pattern.compile("^\\s*+$"); 150 151 /** 152 * Setter to specify the RegExp to match against warnings. Any warning 153 * being suppressed matching this pattern will be flagged. 154 * @param pattern the new pattern 155 */ 156 public final void setFormat(Pattern pattern) { 157 format = pattern; 158 } 159 160 @Override 161 public final int[] getDefaultTokens() { 162 return getAcceptableTokens(); 163 } 164 165 @Override 166 public final int[] getAcceptableTokens() { 167 return new int[] { 168 TokenTypes.CLASS_DEF, 169 TokenTypes.INTERFACE_DEF, 170 TokenTypes.ENUM_DEF, 171 TokenTypes.ANNOTATION_DEF, 172 TokenTypes.ANNOTATION_FIELD_DEF, 173 TokenTypes.ENUM_CONSTANT_DEF, 174 TokenTypes.PARAMETER_DEF, 175 TokenTypes.VARIABLE_DEF, 176 TokenTypes.METHOD_DEF, 177 TokenTypes.CTOR_DEF, 178 }; 179 } 180 181 @Override 182 public int[] getRequiredTokens() { 183 return CommonUtil.EMPTY_INT_ARRAY; 184 } 185 186 @Override 187 public void visitToken(final DetailAST ast) { 188 final DetailAST annotation = getSuppressWarnings(ast); 189 190 if (annotation != null) { 191 final DetailAST warningHolder = 192 findWarningsHolder(annotation); 193 194 final DetailAST token = 195 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 196 DetailAST warning; 197 198 if (token == null) { 199 warning = warningHolder.findFirstToken(TokenTypes.EXPR); 200 } 201 else { 202 // case like '@SuppressWarnings(value = UNUSED)' 203 warning = token.findFirstToken(TokenTypes.EXPR); 204 } 205 206 //rare case with empty array ex: @SuppressWarnings({}) 207 if (warning == null) { 208 //check to see if empty warnings are forbidden -- are by default 209 logMatch(warningHolder, ""); 210 } 211 else { 212 while (warning != null) { 213 if (warning.getType() == TokenTypes.EXPR) { 214 final DetailAST fChild = warning.getFirstChild(); 215 switch (fChild.getType()) { 216 //typical case 217 case TokenTypes.STRING_LITERAL: 218 final String warningText = 219 removeQuotes(warning.getFirstChild().getText()); 220 logMatch(warning, warningText); 221 break; 222 // conditional case 223 // ex: 224 // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 225 case TokenTypes.QUESTION: 226 walkConditional(fChild); 227 break; 228 // param in constant case 229 // ex: public static final String UNCHECKED = "unchecked"; 230 // @SuppressWarnings(UNCHECKED) 231 // or 232 // @SuppressWarnings(SomeClass.UNCHECKED) 233 case TokenTypes.IDENT: 234 case TokenTypes.DOT: 235 break; 236 default: 237 // Known limitation: cases like @SuppressWarnings("un" + "used") or 238 // @SuppressWarnings((String) "unused") are not properly supported, 239 // but they should not cause exceptions. 240 } 241 } 242 warning = warning.getNextSibling(); 243 } 244 } 245 } 246 } 247 248 /** 249 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 250 * that is annotating the AST. If the annotation does not exist 251 * this method will return {@code null}. 252 * 253 * @param ast the AST 254 * @return the {@link SuppressWarnings SuppressWarnings} annotation 255 */ 256 private static DetailAST getSuppressWarnings(DetailAST ast) { 257 DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS); 258 259 if (annotation == null) { 260 annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS); 261 } 262 return annotation; 263 } 264 265 /** 266 * This method looks for a warning that matches a configured expression. 267 * If found it logs a violation at the given AST. 268 * 269 * @param ast the location to place the violation 270 * @param warningText the warning. 271 */ 272 private void logMatch(DetailAST ast, final String warningText) { 273 final Matcher matcher = format.matcher(warningText); 274 if (matcher.matches()) { 275 log(ast, 276 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText); 277 } 278 } 279 280 /** 281 * Find the parent (holder) of the of the warnings (Expr). 282 * 283 * @param annotation the annotation 284 * @return a Token representing the expr. 285 */ 286 private static DetailAST findWarningsHolder(final DetailAST annotation) { 287 final DetailAST annValuePair = 288 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 289 final DetailAST annArrayInit; 290 291 if (annValuePair == null) { 292 annArrayInit = 293 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 294 } 295 else { 296 annArrayInit = 297 annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 298 } 299 300 DetailAST warningsHolder = annotation; 301 if (annArrayInit != null) { 302 warningsHolder = annArrayInit; 303 } 304 305 return warningsHolder; 306 } 307 308 /** 309 * Strips a single double quote from the front and back of a string. 310 * 311 * <p>For example: 312 * <br/> 313 * Input String = "unchecked" 314 * <br/> 315 * Output String = unchecked 316 * 317 * @param warning the warning string 318 * @return the string without two quotes 319 */ 320 private static String removeQuotes(final String warning) { 321 return warning.substring(1, warning.length() - 1); 322 } 323 324 /** 325 * Recursively walks a conditional expression checking the left 326 * and right sides, checking for matches and 327 * logging violations. 328 * 329 * @param cond a Conditional type 330 * {@link TokenTypes#QUESTION QUESTION} 331 */ 332 private void walkConditional(final DetailAST cond) { 333 if (cond.getType() == TokenTypes.QUESTION) { 334 walkConditional(getCondLeft(cond)); 335 walkConditional(getCondRight(cond)); 336 } 337 else { 338 final String warningText = 339 removeQuotes(cond.getText()); 340 logMatch(cond, warningText); 341 } 342 } 343 344 /** 345 * Retrieves the left side of a conditional. 346 * 347 * @param cond cond a conditional type 348 * {@link TokenTypes#QUESTION QUESTION} 349 * @return either the value 350 * or another conditional 351 */ 352 private static DetailAST getCondLeft(final DetailAST cond) { 353 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 354 return colon.getPreviousSibling(); 355 } 356 357 /** 358 * Retrieves the right side of a conditional. 359 * 360 * @param cond a conditional type 361 * {@link TokenTypes#QUESTION QUESTION} 362 * @return either the value 363 * or another conditional 364 */ 365 private static DetailAST getCondRight(final DetailAST cond) { 366 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 367 return colon.getNextSibling(); 368 } 369 370 }