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