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