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 * Default value is {@code "^\s*+$"}. 074 * </li> 075 * <li> 076 * Property {@code tokens} - tokens to check 077 * Default value is: 078 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF"> 079 * CLASS_DEF</a>, 080 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF"> 081 * INTERFACE_DEF</a>, 082 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF"> 083 * ENUM_DEF</a>, 084 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF"> 085 * ANNOTATION_DEF</a>, 086 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF"> 087 * ANNOTATION_FIELD_DEF</a>, 088 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF"> 089 * ENUM_CONSTANT_DEF</a>, 090 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF"> 091 * PARAMETER_DEF</a>, 092 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF"> 093 * VARIABLE_DEF</a>, 094 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF"> 095 * METHOD_DEF</a>, 096 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF"> 097 * CTOR_DEF</a>. 098 * </li> 099 * </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 126public 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 * 155 * @param pattern the new pattern 156 */ 157 public final void setFormat(Pattern pattern) { 158 format = pattern; 159 } 160 161 @Override 162 public final int[] getDefaultTokens() { 163 return getAcceptableTokens(); 164 } 165 166 @Override 167 public final int[] getAcceptableTokens() { 168 return new int[] { 169 TokenTypes.CLASS_DEF, 170 TokenTypes.INTERFACE_DEF, 171 TokenTypes.ENUM_DEF, 172 TokenTypes.ANNOTATION_DEF, 173 TokenTypes.ANNOTATION_FIELD_DEF, 174 TokenTypes.ENUM_CONSTANT_DEF, 175 TokenTypes.PARAMETER_DEF, 176 TokenTypes.VARIABLE_DEF, 177 TokenTypes.METHOD_DEF, 178 TokenTypes.CTOR_DEF, 179 }; 180 } 181 182 @Override 183 public int[] getRequiredTokens() { 184 return CommonUtil.EMPTY_INT_ARRAY; 185 } 186 187 @Override 188 public void visitToken(final DetailAST ast) { 189 final DetailAST annotation = getSuppressWarnings(ast); 190 191 if (annotation != null) { 192 final DetailAST warningHolder = 193 findWarningsHolder(annotation); 194 195 final DetailAST token = 196 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 197 DetailAST warning; 198 199 if (token == null) { 200 warning = warningHolder.findFirstToken(TokenTypes.EXPR); 201 } 202 else { 203 // case like '@SuppressWarnings(value = UNUSED)' 204 warning = token.findFirstToken(TokenTypes.EXPR); 205 } 206 207 // rare case with empty array ex: @SuppressWarnings({}) 208 if (warning == null) { 209 // check to see if empty warnings are forbidden -- are by default 210 logMatch(warningHolder, ""); 211 } 212 else { 213 while (warning != null) { 214 if (warning.getType() == TokenTypes.EXPR) { 215 final DetailAST fChild = warning.getFirstChild(); 216 switch (fChild.getType()) { 217 // typical case 218 case TokenTypes.STRING_LITERAL: 219 final String warningText = 220 removeQuotes(warning.getFirstChild().getText()); 221 logMatch(warning, warningText); 222 break; 223 // conditional case 224 // ex: 225 // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 226 case TokenTypes.QUESTION: 227 walkConditional(fChild); 228 break; 229 // param in constant case 230 // ex: public static final String UNCHECKED = "unchecked"; 231 // @SuppressWarnings(UNCHECKED) 232 // or 233 // @SuppressWarnings(SomeClass.UNCHECKED) 234 case TokenTypes.IDENT: 235 case TokenTypes.DOT: 236 break; 237 default: 238 // Known limitation: cases like @SuppressWarnings("un" + "used") or 239 // @SuppressWarnings((String) "unused") are not properly supported, 240 // but they should not cause exceptions. 241 } 242 } 243 warning = warning.getNextSibling(); 244 } 245 } 246 } 247 } 248 249 /** 250 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 251 * that is annotating the AST. If the annotation does not exist 252 * this method will return {@code null}. 253 * 254 * @param ast the AST 255 * @return the {@link SuppressWarnings SuppressWarnings} annotation 256 */ 257 private static DetailAST getSuppressWarnings(DetailAST ast) { 258 DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS); 259 260 if (annotation == null) { 261 annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS); 262 } 263 return annotation; 264 } 265 266 /** 267 * This method looks for a warning that matches a configured expression. 268 * If found it logs a violation at the given AST. 269 * 270 * @param ast the location to place the violation 271 * @param warningText the warning. 272 */ 273 private void logMatch(DetailAST ast, final String warningText) { 274 final Matcher matcher = format.matcher(warningText); 275 if (matcher.matches()) { 276 log(ast, 277 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText); 278 } 279 } 280 281 /** 282 * Find the parent (holder) of the of the warnings (Expr). 283 * 284 * @param annotation the annotation 285 * @return a Token representing the expr. 286 */ 287 private static DetailAST findWarningsHolder(final DetailAST annotation) { 288 final DetailAST annValuePair = 289 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 290 final DetailAST annArrayInit; 291 292 if (annValuePair == null) { 293 annArrayInit = 294 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 295 } 296 else { 297 annArrayInit = 298 annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 299 } 300 301 DetailAST warningsHolder = annotation; 302 if (annArrayInit != null) { 303 warningsHolder = annArrayInit; 304 } 305 306 return warningsHolder; 307 } 308 309 /** 310 * Strips a single double quote from the front and back of a string. 311 * 312 * <p>For example: 313 * <br/> 314 * Input String = "unchecked" 315 * <br/> 316 * Output String = unchecked 317 * 318 * @param warning the warning string 319 * @return the string without two quotes 320 */ 321 private static String removeQuotes(final String warning) { 322 return warning.substring(1, warning.length() - 1); 323 } 324 325 /** 326 * Recursively walks a conditional expression checking the left 327 * and right sides, checking for matches and 328 * logging violations. 329 * 330 * @param cond a Conditional type 331 * {@link TokenTypes#QUESTION QUESTION} 332 */ 333 private void walkConditional(final DetailAST cond) { 334 if (cond.getType() == TokenTypes.QUESTION) { 335 walkConditional(getCondLeft(cond)); 336 walkConditional(getCondRight(cond)); 337 } 338 else { 339 final String warningText = 340 removeQuotes(cond.getText()); 341 logMatch(cond, warningText); 342 } 343 } 344 345 /** 346 * Retrieves the left side of a conditional. 347 * 348 * @param cond cond a conditional type 349 * {@link TokenTypes#QUESTION QUESTION} 350 * @return either the value 351 * or another conditional 352 */ 353 private static DetailAST getCondLeft(final DetailAST cond) { 354 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 355 return colon.getPreviousSibling(); 356 } 357 358 /** 359 * Retrieves the right side of a conditional. 360 * 361 * @param cond a conditional type 362 * {@link TokenTypes#QUESTION QUESTION} 363 * @return either the value 364 * or another conditional 365 */ 366 private static DetailAST getCondRight(final DetailAST cond) { 367 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 368 return colon.getNextSibling(); 369 } 370 371}