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