View Javadoc
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 &#64;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   * <ul>
78   * <li>
79   * Property {@code format} - Specify the RegExp to match against warnings. Any warning
80   * being suppressed matching this pattern will be flagged.
81   * Type is {@code java.util.regex.Pattern}.
82   * Default value is {@code "^\s*+$"}.
83   * </li>
84   * <li>
85   * Property {@code tokens} - tokens to check
86   * Type is {@code java.lang.String[]}.
87   * Validation type is {@code tokenSet}.
88   * Default value is:
89   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
90   * CLASS_DEF</a>,
91   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
92   * INTERFACE_DEF</a>,
93   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
94   * ENUM_DEF</a>,
95   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
96   * ANNOTATION_DEF</a>,
97   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
98   * ANNOTATION_FIELD_DEF</a>,
99   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
100  * ENUM_CONSTANT_DEF</a>,
101  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF">
102  * PARAMETER_DEF</a>,
103  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
104  * VARIABLE_DEF</a>,
105  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
106  * METHOD_DEF</a>,
107  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
108  * CTOR_DEF</a>,
109  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
110  * COMPACT_CTOR_DEF</a>,
111  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
112  * RECORD_DEF</a>,
113  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PATTERN_VARIABLE_DEF">
114  * PATTERN_VARIABLE_DEF</a>.
115  * </li>
116  * </ul>
117  *
118  * <p>
119  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
120  * </p>
121  *
122  * <p>
123  * Violation Message Keys:
124  * </p>
125  * <ul>
126  * <li>
127  * {@code suppressed.warning.not.allowed}
128  * </li>
129  * </ul>
130  *
131  * @since 5.0
132  */
133 @StatelessCheck
134 public class SuppressWarningsCheck extends AbstractCheck {
135 
136     /**
137      * A key is pointing to the warning message text in "messages.properties"
138      * file.
139      */
140     public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
141         "suppressed.warning.not.allowed";
142 
143     /** {@link SuppressWarnings SuppressWarnings} annotation name. */
144     private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
145 
146     /**
147      * Fully-qualified {@link SuppressWarnings SuppressWarnings}
148      * annotation name.
149      */
150     private static final String FQ_SUPPRESS_WARNINGS =
151         "java.lang." + SUPPRESS_WARNINGS;
152 
153     /**
154      * Specify the RegExp to match against warnings. Any warning
155      * being suppressed matching this pattern will be flagged.
156      */
157     private Pattern format = Pattern.compile("^\\s*+$");
158 
159     /**
160      * Setter to specify the RegExp to match against warnings. Any warning
161      * being suppressed matching this pattern will be flagged.
162      *
163      * @param pattern the new pattern
164      * @since 5.0
165      */
166     public final void setFormat(Pattern pattern) {
167         format = pattern;
168     }
169 
170     @Override
171     public final int[] getDefaultTokens() {
172         return getAcceptableTokens();
173     }
174 
175     @Override
176     public final int[] getAcceptableTokens() {
177         return new int[] {
178             TokenTypes.CLASS_DEF,
179             TokenTypes.INTERFACE_DEF,
180             TokenTypes.ENUM_DEF,
181             TokenTypes.ANNOTATION_DEF,
182             TokenTypes.ANNOTATION_FIELD_DEF,
183             TokenTypes.ENUM_CONSTANT_DEF,
184             TokenTypes.PARAMETER_DEF,
185             TokenTypes.VARIABLE_DEF,
186             TokenTypes.METHOD_DEF,
187             TokenTypes.CTOR_DEF,
188             TokenTypes.COMPACT_CTOR_DEF,
189             TokenTypes.RECORD_DEF,
190             TokenTypes.PATTERN_VARIABLE_DEF,
191         };
192     }
193 
194     @Override
195     public int[] getRequiredTokens() {
196         return CommonUtil.EMPTY_INT_ARRAY;
197     }
198 
199     @Override
200     public void visitToken(final DetailAST ast) {
201         final DetailAST annotation = getSuppressWarnings(ast);
202 
203         if (annotation != null) {
204             final DetailAST warningHolder =
205                 findWarningsHolder(annotation);
206             final DetailAST token =
207                     warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
208 
209             // case like '@SuppressWarnings(value = UNUSED)'
210             final DetailAST parent = Objects.requireNonNullElse(token, warningHolder);
211             final DetailAST warning = parent.findFirstToken(TokenTypes.EXPR);
212 
213             if (warning == null) {
214                 // check to see if empty warnings are forbidden -- are by default
215                 logMatch(warningHolder, "");
216             }
217             else {
218                 processWarnings(warning);
219             }
220         }
221     }
222 
223     /**
224      * Processes all warning expressions starting from the given AST node.
225      *
226      * @param warning the first warning expression node to process
227      */
228     private void processWarnings(final DetailAST warning) {
229         for (DetailAST current = warning; current != null; current = current.getNextSibling()) {
230             if (current.getType() == TokenTypes.EXPR) {
231                 processWarningExpr(current.getFirstChild(), current);
232             }
233         }
234     }
235 
236     /**
237      * Processes a single warning expression.
238      *
239      * @param fChild  the first child AST of the expression
240      * @param warning the parent warning AST node
241      */
242     private void processWarningExpr(final DetailAST fChild, final DetailAST warning) {
243         switch (fChild.getType()) {
244             case TokenTypes.STRING_LITERAL -> logMatch(warning,
245                     removeQuotes(warning.getFirstChild().getText()));
246 
247             case TokenTypes.QUESTION ->
248                 // ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
249                 walkConditional(fChild);
250 
251             default -> {
252             // Known limitation: cases like @SuppressWarnings("un" + "used") or
253             // @SuppressWarnings((String) "unused") are not properly supported,
254             // but they should not cause exceptions.
255             // Also constants as params:
256             // ex: public static final String UNCHECKED = "unchecked";
257             // @SuppressWarnings(UNCHECKED)
258             // or
259             // @SuppressWarnings(SomeClass.UNCHECKED)
260             }
261         }
262     }
263 
264     /**
265      * Gets the {@link SuppressWarnings SuppressWarnings} annotation
266      * that is annotating the AST.  If the annotation does not exist
267      * this method will return {@code null}.
268      *
269      * @param ast the AST
270      * @return the {@link SuppressWarnings SuppressWarnings} annotation
271      */
272     private static DetailAST getSuppressWarnings(DetailAST ast) {
273         DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS);
274 
275         if (annotation == null) {
276             annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
277         }
278         return annotation;
279     }
280 
281     /**
282      * This method looks for a warning that matches a configured expression.
283      * If found it logs a violation at the given AST.
284      *
285      * @param ast the location to place the violation
286      * @param warningText the warning.
287      */
288     private void logMatch(DetailAST ast, final String warningText) {
289         final Matcher matcher = format.matcher(warningText);
290         if (matcher.matches()) {
291             log(ast,
292                     MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
293         }
294     }
295 
296     /**
297      * Find the parent (holder) of the of the warnings (Expr).
298      *
299      * @param annotation the annotation
300      * @return a Token representing the expr.
301      */
302     private static DetailAST findWarningsHolder(final DetailAST annotation) {
303         final DetailAST annValuePair =
304             annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
305 
306         final DetailAST annArrayInitParent = Objects.requireNonNullElse(annValuePair, annotation);
307         final DetailAST annArrayInit = annArrayInitParent
308                 .findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
309         return Objects.requireNonNullElse(annArrayInit, annotation);
310     }
311 
312     /**
313      * Strips a single double quote from the front and back of a string.
314      *
315      * <p>For example:</p>
316      * <pre>
317      * Input String = "unchecked"
318      * </pre>
319      * Output String = unchecked
320      *
321      * @param warning the warning string
322      * @return the string without two quotes
323      */
324     private static String removeQuotes(final String warning) {
325         return warning.substring(1, warning.length() - 1);
326     }
327 
328     /**
329      * Walks a conditional expression checking the left
330      * and right sides, checking for matches and
331      * logging violations.
332      *
333      * @param cond a Conditional type
334      *     {@link TokenTypes#QUESTION QUESTION}
335      */
336     private void walkConditional(final DetailAST cond) {
337         final Deque<DetailAST> condStack = new ArrayDeque<>();
338         condStack.push(cond);
339 
340         while (!condStack.isEmpty()) {
341             final DetailAST currentCond = condStack.pop();
342             if (currentCond.getType() == TokenTypes.QUESTION) {
343                 condStack.push(getCondRight(currentCond));
344                 condStack.push(getCondLeft(currentCond));
345             }
346             else {
347                 final String warningText = removeQuotes(currentCond.getText());
348                 logMatch(currentCond, warningText);
349             }
350         }
351     }
352 
353     /**
354      * Retrieves the left side of a conditional.
355      *
356      * @param cond cond a conditional type
357      *     {@link TokenTypes#QUESTION QUESTION}
358      * @return either the value
359      *     or another conditional
360      */
361     private static DetailAST getCondLeft(final DetailAST cond) {
362         final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
363         return colon.getPreviousSibling();
364     }
365 
366     /**
367      * Retrieves the right side of a conditional.
368      *
369      * @param cond a conditional type
370      *     {@link TokenTypes#QUESTION QUESTION}
371      * @return either the value
372      *     or another conditional
373      */
374     private static DetailAST getCondRight(final DetailAST cond) {
375         final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
376         return colon.getNextSibling();
377     }
378 
379 }