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