View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Objects;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
31  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32  
33  /**
34   * <p>
35   * Allows to specify what warnings that
36   * {@code @SuppressWarnings} is not allowed to suppress.
37   * You can also specify a list of TokenTypes that
38   * the configured warning(s) cannot be suppressed on.
39   * </p>
40   * <p>
41   * Limitations:  This check does not consider conditionals
42   * inside the &#64;SuppressWarnings annotation.
43   * </p>
44   * <p>
45   * For example:
46   * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}.
47   * According to the above example, the "unused" warning is being suppressed
48   * not the "unchecked" or "foo" warnings.  All of these warnings will be
49   * considered and matched against regardless of what the conditional
50   * evaluates to.
51   * The check also does not support code like {@code @SuppressWarnings("un" + "used")},
52   * {@code @SuppressWarnings((String) "unused")} or
53   * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}.
54   * </p>
55   * <p>
56   * By default, any warning specified will be disallowed on
57   * all legal TokenTypes unless otherwise specified via
58   * the tokens property.
59   * </p>
60   * <p>
61   * Also, by default warnings that are empty strings or all
62   * whitespace (regex: ^$|^\s+$) are flagged.  By specifying,
63   * the format property these defaults no longer apply.
64   * </p>
65   * <p>This check can be configured so that the "unchecked"
66   * and "unused" warnings cannot be suppressed on
67   * anything but variable and parameter declarations.
68   * See below of an example.
69   * </p>
70   * <ul>
71   * <li>
72   * Property {@code format} - Specify the RegExp to match against warnings. Any warning
73   * being suppressed matching this pattern will be flagged.
74   * Type is {@code java.util.regex.Pattern}.
75   * Default value is {@code "^\s*+$"}.
76   * </li>
77   * <li>
78   * Property {@code tokens} - tokens to check
79   * Type is {@code java.lang.String[]}.
80   * Validation type is {@code tokenSet}.
81   * Default value is:
82   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
83   * CLASS_DEF</a>,
84   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
85   * INTERFACE_DEF</a>,
86   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
87   * ENUM_DEF</a>,
88   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
89   * ANNOTATION_DEF</a>,
90   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
91   * ANNOTATION_FIELD_DEF</a>,
92   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
93   * ENUM_CONSTANT_DEF</a>,
94   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF">
95   * PARAMETER_DEF</a>,
96   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
97   * VARIABLE_DEF</a>,
98   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
99   * METHOD_DEF</a>,
100  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
101  * CTOR_DEF</a>,
102  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
103  * COMPACT_CTOR_DEF</a>,
104  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
105  * RECORD_DEF</a>,
106  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PATTERN_VARIABLE_DEF">
107  * PATTERN_VARIABLE_DEF</a>.
108  * </li>
109  * </ul>
110  * <p>
111  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
112  * </p>
113  * <p>
114  * Violation Message Keys:
115  * </p>
116  * <ul>
117  * <li>
118  * {@code suppressed.warning.not.allowed}
119  * </li>
120  * </ul>
121  *
122  * @since 5.0
123  */
124 @StatelessCheck
125 public class SuppressWarningsCheck extends AbstractCheck {
126 
127     /**
128      * A key is pointing to the warning message text in "messages.properties"
129      * file.
130      */
131     public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
132         "suppressed.warning.not.allowed";
133 
134     /** {@link SuppressWarnings SuppressWarnings} annotation name. */
135     private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
136 
137     /**
138      * Fully-qualified {@link SuppressWarnings SuppressWarnings}
139      * annotation name.
140      */
141     private static final String FQ_SUPPRESS_WARNINGS =
142         "java.lang." + SUPPRESS_WARNINGS;
143 
144     /**
145      * Specify the RegExp to match against warnings. Any warning
146      * being suppressed matching this pattern will be flagged.
147      */
148     private Pattern format = Pattern.compile("^\\s*+$");
149 
150     /**
151      * Setter to specify the RegExp to match against warnings. Any warning
152      * being suppressed matching this pattern will be flagged.
153      *
154      * @param pattern the new pattern
155      * @since 5.0
156      */
157     public final void setFormat(Pattern pattern) {
158         format = pattern;
159     }
160 
161     @Override
162     public final int[] getDefaultTokens() {
163         return getAcceptableTokens();
164     }
165 
166     @Override
167     public final int[] getAcceptableTokens() {
168         return new int[] {
169             TokenTypes.CLASS_DEF,
170             TokenTypes.INTERFACE_DEF,
171             TokenTypes.ENUM_DEF,
172             TokenTypes.ANNOTATION_DEF,
173             TokenTypes.ANNOTATION_FIELD_DEF,
174             TokenTypes.ENUM_CONSTANT_DEF,
175             TokenTypes.PARAMETER_DEF,
176             TokenTypes.VARIABLE_DEF,
177             TokenTypes.METHOD_DEF,
178             TokenTypes.CTOR_DEF,
179             TokenTypes.COMPACT_CTOR_DEF,
180             TokenTypes.RECORD_DEF,
181             TokenTypes.PATTERN_VARIABLE_DEF,
182         };
183     }
184 
185     @Override
186     public int[] getRequiredTokens() {
187         return CommonUtil.EMPTY_INT_ARRAY;
188     }
189 
190     @Override
191     public void visitToken(final DetailAST ast) {
192         final DetailAST annotation = getSuppressWarnings(ast);
193 
194         if (annotation != null) {
195             final DetailAST warningHolder =
196                 findWarningsHolder(annotation);
197 
198             final DetailAST token =
199                     warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
200 
201             // case like '@SuppressWarnings(value = UNUSED)'
202             final DetailAST parent = Objects.requireNonNullElse(token, warningHolder);
203             DetailAST warning = parent.findFirstToken(TokenTypes.EXPR);
204 
205             // rare case with empty array ex: @SuppressWarnings({})
206             if (warning == null) {
207                 // check to see if empty warnings are forbidden -- are by default
208                 logMatch(warningHolder, "");
209             }
210             else {
211                 while (warning != null) {
212                     if (warning.getType() == TokenTypes.EXPR) {
213                         final DetailAST fChild = warning.getFirstChild();
214                         switch (fChild.getType()) {
215                             // typical case
216                             case TokenTypes.STRING_LITERAL:
217                                 final String warningText =
218                                     removeQuotes(warning.getFirstChild().getText());
219                                 logMatch(warning, warningText);
220                                 break;
221                             // conditional case
222                             // ex:
223                             // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
224                             case TokenTypes.QUESTION:
225                                 walkConditional(fChild);
226                                 break;
227                             default:
228                                 // Known limitation: cases like @SuppressWarnings("un" + "used") or
229                                 // @SuppressWarnings((String) "unused") are not properly supported,
230                                 // but they should not cause exceptions.
231                                 // Also constant as param
232                                 // ex: public static final String UNCHECKED = "unchecked";
233                                 // @SuppressWarnings(UNCHECKED)
234                                 // or
235                                 // @SuppressWarnings(SomeClass.UNCHECKED)
236                         }
237                     }
238                     warning = warning.getNextSibling();
239                 }
240             }
241         }
242     }
243 
244     /**
245      * Gets the {@link SuppressWarnings SuppressWarnings} annotation
246      * that is annotating the AST.  If the annotation does not exist
247      * this method will return {@code null}.
248      *
249      * @param ast the AST
250      * @return the {@link SuppressWarnings SuppressWarnings} annotation
251      */
252     private static DetailAST getSuppressWarnings(DetailAST ast) {
253         DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS);
254 
255         if (annotation == null) {
256             annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
257         }
258         return annotation;
259     }
260 
261     /**
262      * This method looks for a warning that matches a configured expression.
263      * If found it logs a violation at the given AST.
264      *
265      * @param ast the location to place the violation
266      * @param warningText the warning.
267      */
268     private void logMatch(DetailAST ast, final String warningText) {
269         final Matcher matcher = format.matcher(warningText);
270         if (matcher.matches()) {
271             log(ast,
272                     MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
273         }
274     }
275 
276     /**
277      * Find the parent (holder) of the of the warnings (Expr).
278      *
279      * @param annotation the annotation
280      * @return a Token representing the expr.
281      */
282     private static DetailAST findWarningsHolder(final DetailAST annotation) {
283         final DetailAST annValuePair =
284             annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
285 
286         final DetailAST annArrayInitParent = Objects.requireNonNullElse(annValuePair, annotation);
287         final DetailAST annArrayInit = annArrayInitParent
288                 .findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
289         return Objects.requireNonNullElse(annArrayInit, annotation);
290     }
291 
292     /**
293      * Strips a single double quote from the front and back of a string.
294      *
295      * <p>For example:</p>
296      * <pre>
297      * Input String = "unchecked"
298      * </pre>
299      * Output String = unchecked
300      *
301      * @param warning the warning string
302      * @return the string without two quotes
303      */
304     private static String removeQuotes(final String warning) {
305         return warning.substring(1, warning.length() - 1);
306     }
307 
308     /**
309      * Recursively walks a conditional expression checking the left
310      * and right sides, checking for matches and
311      * logging violations.
312      *
313      * @param cond a Conditional type
314      *     {@link TokenTypes#QUESTION QUESTION}
315      * @noinspection TailRecursion
316      * @noinspectionreason TailRecursion - until issue #14814
317      */
318     private void walkConditional(final DetailAST cond) {
319         if (cond.getType() == TokenTypes.QUESTION) {
320             walkConditional(getCondLeft(cond));
321             walkConditional(getCondRight(cond));
322         }
323         else {
324             final String warningText =
325                     removeQuotes(cond.getText());
326             logMatch(cond, warningText);
327         }
328     }
329 
330     /**
331      * Retrieves the left side of a conditional.
332      *
333      * @param cond cond a conditional type
334      *     {@link TokenTypes#QUESTION QUESTION}
335      * @return either the value
336      *     or another conditional
337      */
338     private static DetailAST getCondLeft(final DetailAST cond) {
339         final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
340         return colon.getPreviousSibling();
341     }
342 
343     /**
344      * Retrieves the right side of a conditional.
345      *
346      * @param cond a conditional type
347      *     {@link TokenTypes#QUESTION QUESTION}
348      * @return either the value
349      *     or another conditional
350      */
351     private static DetailAST getCondRight(final DetailAST cond) {
352         final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
353         return colon.getNextSibling();
354     }
355 
356 }