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