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