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