1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2024 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.Objects; 23 import java.util.regex.Matcher; 24 import java.util.regex.Pattern; 25 26 import com.puppycrawl.tools.checkstyle.StatelessCheck; 27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 28 import com.puppycrawl.tools.checkstyle.api.DetailAST; 29 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 30 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 32 33 /** 34 * <p> 35 * Allows to specify what warnings that 36 * {@code @SuppressWarnings} is not allowed to suppress. 37 * You can also specify a list of TokenTypes that 38 * the configured warning(s) cannot be suppressed on. 39 * </p> 40 * <p> 41 * Limitations: This check does not consider conditionals 42 * inside the @SuppressWarnings annotation. 43 * </p> 44 * <p> 45 * For example: 46 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}. 47 * According to the above example, the "unused" warning is being suppressed 48 * not the "unchecked" or "foo" warnings. All of these warnings will be 49 * considered and matched against regardless of what the conditional 50 * evaluates to. 51 * The check also does not support code like {@code @SuppressWarnings("un" + "used")}, 52 * {@code @SuppressWarnings((String) "unused")} or 53 * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}. 54 * </p> 55 * <p> 56 * By default, any warning specified will be disallowed on 57 * all legal TokenTypes unless otherwise specified via 58 * the tokens property. 59 * </p> 60 * <p> 61 * Also, by default warnings that are empty strings or all 62 * whitespace (regex: ^$|^\s+$) are flagged. By specifying, 63 * the format property these defaults no longer apply. 64 * </p> 65 * <p>This check can be configured so that the "unchecked" 66 * and "unused" warnings cannot be suppressed on 67 * anything but variable and parameter declarations. 68 * See below of an example. 69 * </p> 70 * <ul> 71 * <li> 72 * Property {@code format} - Specify the RegExp to match against warnings. Any warning 73 * being suppressed matching this pattern will be flagged. 74 * Type is {@code java.util.regex.Pattern}. 75 * Default value is {@code "^\s*+$"}. 76 * </li> 77 * <li> 78 * Property {@code tokens} - tokens to check 79 * Type is {@code java.lang.String[]}. 80 * Validation type is {@code tokenSet}. 81 * Default value is: 82 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF"> 83 * CLASS_DEF</a>, 84 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF"> 85 * INTERFACE_DEF</a>, 86 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF"> 87 * ENUM_DEF</a>, 88 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF"> 89 * ANNOTATION_DEF</a>, 90 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF"> 91 * ANNOTATION_FIELD_DEF</a>, 92 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF"> 93 * ENUM_CONSTANT_DEF</a>, 94 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF"> 95 * PARAMETER_DEF</a>, 96 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF"> 97 * VARIABLE_DEF</a>, 98 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF"> 99 * 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 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PATTERN_VARIABLE_DEF"> 107 * PATTERN_VARIABLE_DEF</a>. 108 * </li> 109 * </ul> 110 * <p> 111 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 112 * </p> 113 * <p> 114 * Violation Message Keys: 115 * </p> 116 * <ul> 117 * <li> 118 * {@code suppressed.warning.not.allowed} 119 * </li> 120 * </ul> 121 * 122 * @since 5.0 123 */ 124 @StatelessCheck 125 public class SuppressWarningsCheck extends AbstractCheck { 126 127 /** 128 * A key is pointing to the warning message text in "messages.properties" 129 * file. 130 */ 131 public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED = 132 "suppressed.warning.not.allowed"; 133 134 /** {@link SuppressWarnings SuppressWarnings} annotation name. */ 135 private static final String SUPPRESS_WARNINGS = "SuppressWarnings"; 136 137 /** 138 * Fully-qualified {@link SuppressWarnings SuppressWarnings} 139 * annotation name. 140 */ 141 private static final String FQ_SUPPRESS_WARNINGS = 142 "java.lang." + SUPPRESS_WARNINGS; 143 144 /** 145 * Specify the RegExp to match against warnings. Any warning 146 * being suppressed matching this pattern will be flagged. 147 */ 148 private Pattern format = Pattern.compile("^\\s*+$"); 149 150 /** 151 * Setter to specify the RegExp to match against warnings. Any warning 152 * being suppressed matching this pattern will be flagged. 153 * 154 * @param pattern the new pattern 155 * @since 5.0 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 TokenTypes.COMPACT_CTOR_DEF, 180 TokenTypes.RECORD_DEF, 181 TokenTypes.PATTERN_VARIABLE_DEF, 182 }; 183 } 184 185 @Override 186 public int[] getRequiredTokens() { 187 return CommonUtil.EMPTY_INT_ARRAY; 188 } 189 190 @Override 191 public void visitToken(final DetailAST ast) { 192 final DetailAST annotation = getSuppressWarnings(ast); 193 194 if (annotation != null) { 195 final DetailAST warningHolder = 196 findWarningsHolder(annotation); 197 198 final DetailAST token = 199 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 200 201 // case like '@SuppressWarnings(value = UNUSED)' 202 final DetailAST parent = Objects.requireNonNullElse(token, warningHolder); 203 DetailAST warning = parent.findFirstToken(TokenTypes.EXPR); 204 205 // rare case with empty array ex: @SuppressWarnings({}) 206 if (warning == null) { 207 // check to see if empty warnings are forbidden -- are by default 208 logMatch(warningHolder, ""); 209 } 210 else { 211 while (warning != null) { 212 if (warning.getType() == TokenTypes.EXPR) { 213 final DetailAST fChild = warning.getFirstChild(); 214 switch (fChild.getType()) { 215 // typical case 216 case TokenTypes.STRING_LITERAL: 217 final String warningText = 218 removeQuotes(warning.getFirstChild().getText()); 219 logMatch(warning, warningText); 220 break; 221 // conditional case 222 // ex: 223 // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 224 case TokenTypes.QUESTION: 225 walkConditional(fChild); 226 break; 227 default: 228 // Known limitation: cases like @SuppressWarnings("un" + "used") or 229 // @SuppressWarnings((String) "unused") are not properly supported, 230 // but they should not cause exceptions. 231 // Also constant as param 232 // ex: public static final String UNCHECKED = "unchecked"; 233 // @SuppressWarnings(UNCHECKED) 234 // or 235 // @SuppressWarnings(SomeClass.UNCHECKED) 236 } 237 } 238 warning = warning.getNextSibling(); 239 } 240 } 241 } 242 } 243 244 /** 245 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 246 * that is annotating the AST. If the annotation does not exist 247 * this method will return {@code null}. 248 * 249 * @param ast the AST 250 * @return the {@link SuppressWarnings SuppressWarnings} annotation 251 */ 252 private static DetailAST getSuppressWarnings(DetailAST ast) { 253 DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS); 254 255 if (annotation == null) { 256 annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS); 257 } 258 return annotation; 259 } 260 261 /** 262 * This method looks for a warning that matches a configured expression. 263 * If found it logs a violation at the given AST. 264 * 265 * @param ast the location to place the violation 266 * @param warningText the warning. 267 */ 268 private void logMatch(DetailAST ast, final String warningText) { 269 final Matcher matcher = format.matcher(warningText); 270 if (matcher.matches()) { 271 log(ast, 272 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText); 273 } 274 } 275 276 /** 277 * Find the parent (holder) of the of the warnings (Expr). 278 * 279 * @param annotation the annotation 280 * @return a Token representing the expr. 281 */ 282 private static DetailAST findWarningsHolder(final DetailAST annotation) { 283 final DetailAST annValuePair = 284 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 285 286 final DetailAST annArrayInitParent = Objects.requireNonNullElse(annValuePair, annotation); 287 final DetailAST annArrayInit = annArrayInitParent 288 .findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 289 return Objects.requireNonNullElse(annArrayInit, annotation); 290 } 291 292 /** 293 * Strips a single double quote from the front and back of a string. 294 * 295 * <p>For example:</p> 296 * <pre> 297 * Input String = "unchecked" 298 * </pre> 299 * Output String = unchecked 300 * 301 * @param warning the warning string 302 * @return the string without two quotes 303 */ 304 private static String removeQuotes(final String warning) { 305 return warning.substring(1, warning.length() - 1); 306 } 307 308 /** 309 * Recursively walks a conditional expression checking the left 310 * and right sides, checking for matches and 311 * logging violations. 312 * 313 * @param cond a Conditional type 314 * {@link TokenTypes#QUESTION QUESTION} 315 * @noinspection TailRecursion 316 * @noinspectionreason TailRecursion - until issue #14814 317 */ 318 private void walkConditional(final DetailAST cond) { 319 if (cond.getType() == TokenTypes.QUESTION) { 320 walkConditional(getCondLeft(cond)); 321 walkConditional(getCondRight(cond)); 322 } 323 else { 324 final String warningText = 325 removeQuotes(cond.getText()); 326 logMatch(cond, warningText); 327 } 328 } 329 330 /** 331 * Retrieves the left side of a conditional. 332 * 333 * @param cond cond a conditional type 334 * {@link TokenTypes#QUESTION QUESTION} 335 * @return either the value 336 * or another conditional 337 */ 338 private static DetailAST getCondLeft(final DetailAST cond) { 339 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 340 return colon.getPreviousSibling(); 341 } 342 343 /** 344 * Retrieves the right side of a conditional. 345 * 346 * @param cond a conditional type 347 * {@link TokenTypes#QUESTION QUESTION} 348 * @return either the value 349 * or another conditional 350 */ 351 private static DetailAST getCondRight(final DetailAST cond) { 352 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 353 return colon.getNextSibling(); 354 } 355 356 }