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 *
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 * Setter to specify the RegExp to match against warnings. Any warning
108 * being suppressed matching this pattern will be flagged.
109 *
110 * @param pattern the new pattern
111 * @since 5.0
112 */
113 public final void setFormat(Pattern pattern) {
114 format = pattern;
115 }
116
117 @Override
118 public final int[] getDefaultTokens() {
119 return getAcceptableTokens();
120 }
121
122 @Override
123 public final int[] getAcceptableTokens() {
124 return new int[] {
125 TokenTypes.CLASS_DEF,
126 TokenTypes.INTERFACE_DEF,
127 TokenTypes.ENUM_DEF,
128 TokenTypes.ANNOTATION_DEF,
129 TokenTypes.ANNOTATION_FIELD_DEF,
130 TokenTypes.ENUM_CONSTANT_DEF,
131 TokenTypes.PARAMETER_DEF,
132 TokenTypes.VARIABLE_DEF,
133 TokenTypes.METHOD_DEF,
134 TokenTypes.CTOR_DEF,
135 TokenTypes.COMPACT_CTOR_DEF,
136 TokenTypes.RECORD_DEF,
137 TokenTypes.PATTERN_VARIABLE_DEF,
138 };
139 }
140
141 @Override
142 public int[] getRequiredTokens() {
143 return CommonUtil.EMPTY_INT_ARRAY;
144 }
145
146 @Override
147 public void visitToken(final DetailAST ast) {
148 final DetailAST annotation = getSuppressWarnings(ast);
149
150 if (annotation != null) {
151 final DetailAST warningHolder =
152 findWarningsHolder(annotation);
153 final DetailAST token =
154 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
155
156 // case like '@SuppressWarnings(value = UNUSED)'
157 final DetailAST parent = Objects.requireNonNullElse(token, warningHolder);
158 final DetailAST warning = parent.findFirstToken(TokenTypes.EXPR);
159
160 if (warning == null) {
161 // check to see if empty warnings are forbidden -- are by default
162 logMatch(warningHolder, "");
163 }
164 else {
165 processWarnings(warning);
166 }
167 }
168 }
169
170 /**
171 * Processes all warning expressions starting from the given AST node.
172 *
173 * @param warning the first warning expression node to process
174 */
175 private void processWarnings(final DetailAST warning) {
176 for (DetailAST current = warning; current != null; current = current.getNextSibling()) {
177 if (current.getType() == TokenTypes.EXPR) {
178 processWarningExpr(current.getFirstChild(), current);
179 }
180 }
181 }
182
183 /**
184 * Processes a single warning expression.
185 *
186 * @param fChild the first child AST of the expression
187 * @param warning the parent warning AST node
188 */
189 private void processWarningExpr(final DetailAST fChild, final DetailAST warning) {
190 switch (fChild.getType()) {
191 case TokenTypes.STRING_LITERAL -> logMatch(warning,
192 removeQuotes(warning.getFirstChild().getText()));
193
194 case TokenTypes.QUESTION ->
195 // ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
196 walkConditional(fChild);
197
198 default -> {
199 // Known limitation: cases like @SuppressWarnings("un" + "used") or
200 // @SuppressWarnings((String) "unused") are not properly supported,
201 // but they should not cause exceptions.
202 // Also constants as params:
203 // ex: public static final String UNCHECKED = "unchecked";
204 // @SuppressWarnings(UNCHECKED)
205 // or
206 // @SuppressWarnings(SomeClass.UNCHECKED)
207 }
208 }
209 }
210
211 /**
212 * Gets the {@link SuppressWarnings SuppressWarnings} annotation
213 * that is annotating the AST. If the annotation does not exist
214 * this method will return {@code null}.
215 *
216 * @param ast the AST
217 * @return the {@link SuppressWarnings SuppressWarnings} annotation
218 */
219 private static DetailAST getSuppressWarnings(DetailAST ast) {
220 DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS);
221
222 if (annotation == null) {
223 annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
224 }
225 return annotation;
226 }
227
228 /**
229 * This method looks for a warning that matches a configured expression.
230 * If found it logs a violation at the given AST.
231 *
232 * @param ast the location to place the violation
233 * @param warningText the warning.
234 */
235 private void logMatch(DetailAST ast, final String warningText) {
236 final Matcher matcher = format.matcher(warningText);
237 if (matcher.matches()) {
238 log(ast,
239 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
240 }
241 }
242
243 /**
244 * Find the parent (holder) of the of the warnings (Expr).
245 *
246 * @param annotation the annotation
247 * @return a Token representing the expr.
248 */
249 private static DetailAST findWarningsHolder(final DetailAST annotation) {
250 final DetailAST annValuePair =
251 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
252
253 final DetailAST annArrayInitParent = Objects.requireNonNullElse(annValuePair, annotation);
254 final DetailAST annArrayInit = annArrayInitParent
255 .findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
256 return Objects.requireNonNullElse(annArrayInit, annotation);
257 }
258
259 /**
260 * Strips a single double quote from the front and back of a string.
261 *
262 * <p>For example:</p>
263 * <pre>
264 * Input String = "unchecked"
265 * </pre>
266 * Output String = unchecked
267 *
268 * @param warning the warning string
269 * @return the string without two quotes
270 */
271 private static String removeQuotes(final String warning) {
272 return warning.substring(1, warning.length() - 1);
273 }
274
275 /**
276 * Walks a conditional expression checking the left
277 * and right sides, checking for matches and
278 * logging violations.
279 *
280 * @param cond a Conditional type
281 * {@link TokenTypes#QUESTION QUESTION}
282 */
283 private void walkConditional(final DetailAST cond) {
284 final Deque<DetailAST> condStack = new ArrayDeque<>();
285 condStack.push(cond);
286
287 while (!condStack.isEmpty()) {
288 final DetailAST currentCond = condStack.pop();
289 if (currentCond.getType() == TokenTypes.QUESTION) {
290 condStack.push(getCondRight(currentCond));
291 condStack.push(getCondLeft(currentCond));
292 }
293 else {
294 final String warningText = removeQuotes(currentCond.getText());
295 logMatch(currentCond, warningText);
296 }
297 }
298 }
299
300 /**
301 * Retrieves the left side of a conditional.
302 *
303 * @param cond cond a conditional type
304 * {@link TokenTypes#QUESTION QUESTION}
305 * @return either the value
306 * or another conditional
307 */
308 private static DetailAST getCondLeft(final DetailAST cond) {
309 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
310 return colon.getPreviousSibling();
311 }
312
313 /**
314 * Retrieves the right side of a conditional.
315 *
316 * @param cond a conditional type
317 * {@link TokenTypes#QUESTION QUESTION}
318 * @return either the value
319 * or another conditional
320 */
321 private static DetailAST getCondRight(final DetailAST cond) {
322 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
323 return colon.getNextSibling();
324 }
325
326 }