1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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 *
78 * @since 5.0
79 */
80 @StatelessCheck
81 public class SuppressWarningsCheck extends AbstractCheck {
82
83 /**
84 * A key is pointing to the warning message text in "messages.properties"
85 * file.
86 */
87 public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
88 "suppressed.warning.not.allowed";
89
90 /** {@link SuppressWarnings SuppressWarnings} annotation name. */
91 private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
92
93 /**
94 * Fully-qualified {@link SuppressWarnings SuppressWarnings}
95 * annotation name.
96 */
97 private static final String FQ_SUPPRESS_WARNINGS =
98 "java.lang." + SUPPRESS_WARNINGS;
99
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 }