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