001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.annotation;
021
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
030
031/**
032 * <p>
033 * This check allows you to specify what warnings that
034 * {@link SuppressWarnings SuppressWarnings} is not
035 * allowed to suppress.  You can also specify a list
036 * of TokenTypes that the configured warning(s) cannot
037 * be suppressed on.
038 * </p>
039 *
040 * <p>
041 * The {@link #setFormat warnings} property is a
042 * regex pattern.  Any warning being suppressed matching
043 * this pattern will be flagged.
044 * </p>
045 *
046 * <p>
047 * By default, any warning specified will be disallowed on
048 * all legal TokenTypes unless otherwise specified via
049 * the
050 * {@link AbstractCheck#setTokens(String[]) tokens}
051 * property.
052 *
053 * Also, by default warnings that are empty strings or all
054 * whitespace (regex: ^$|^\s+$) are flagged.  By specifying,
055 * the format property these defaults no longer apply.
056 * </p>
057 *
058 * <p>Limitations:  This check does not consider conditionals
059 * inside the SuppressWarnings annotation. <br>
060 * For example:
061 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}.
062 * According to the above example, the "unused" warning is being suppressed
063 * not the "unchecked" or "foo" warnings.  All of these warnings will be
064 * considered and matched against regardless of what the conditional
065 * evaluates to.
066 * <br>
067 * The check also does not support code like {@code @SuppressWarnings("un" + "used")},
068 * {@code @SuppressWarnings((String) "unused")} or
069 * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}.
070 * </p>
071 *
072 * <p>This check can be configured so that the "unchecked"
073 * and "unused" warnings cannot be suppressed on
074 * anything but variable and parameter declarations.
075 * See below of an example.
076 * </p>
077 *
078 * <pre>
079 * &lt;module name=&quot;SuppressWarnings&quot;&gt;
080 *    &lt;property name=&quot;format&quot;
081 *        value=&quot;^unchecked$|^unused$&quot;/&gt;
082 *    &lt;property name=&quot;tokens&quot;
083 *        value=&quot;
084 *        CLASS_DEF,INTERFACE_DEF,ENUM_DEF,
085 *        ANNOTATION_DEF,ANNOTATION_FIELD_DEF,
086 *        ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF
087 *        &quot;/&gt;
088 * &lt;/module&gt;
089 * </pre>
090 * @author Travis Schneeberger
091 */
092public class SuppressWarningsCheck extends AbstractCheck {
093    /**
094     * A key is pointing to the warning message text in "messages.properties"
095     * file.
096     */
097    public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
098        "suppressed.warning.not.allowed";
099
100    /** {@link SuppressWarnings SuppressWarnings} annotation name. */
101    private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
102
103    /**
104     * Fully-qualified {@link SuppressWarnings SuppressWarnings}
105     * annotation name.
106     */
107    private static final String FQ_SUPPRESS_WARNINGS =
108        "java.lang." + SUPPRESS_WARNINGS;
109
110    /** The format string of the regexp. */
111    private String format = "^$|^\\s+$";
112
113    /** The regexp to match against. */
114    private Pattern regexp = Pattern.compile(format);
115
116    /**
117     * Set the format to the specified regular expression.
118     * @param format a {@code String} value
119     * @throws org.apache.commons.beanutils.ConversionException unable to parse format
120     */
121    public final void setFormat(String format) {
122        this.format = format;
123        regexp = CommonUtils.createPattern(format);
124    }
125
126    @Override
127    public final int[] getDefaultTokens() {
128        return getAcceptableTokens();
129    }
130
131    @Override
132    public final int[] getAcceptableTokens() {
133        return new int[] {
134            TokenTypes.CLASS_DEF,
135            TokenTypes.INTERFACE_DEF,
136            TokenTypes.ENUM_DEF,
137            TokenTypes.ANNOTATION_DEF,
138            TokenTypes.ANNOTATION_FIELD_DEF,
139            TokenTypes.ENUM_CONSTANT_DEF,
140            TokenTypes.PARAMETER_DEF,
141            TokenTypes.VARIABLE_DEF,
142            TokenTypes.METHOD_DEF,
143            TokenTypes.CTOR_DEF,
144        };
145    }
146
147    @Override
148    public int[] getRequiredTokens() {
149        return CommonUtils.EMPTY_INT_ARRAY;
150    }
151
152    @Override
153    public void visitToken(final DetailAST ast) {
154        final DetailAST annotation = getSuppressWarnings(ast);
155
156        if (annotation != null) {
157            final DetailAST warningHolder =
158                findWarningsHolder(annotation);
159
160            final DetailAST token =
161                    warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
162            DetailAST warning;
163
164            if (token == null) {
165                warning = warningHolder.findFirstToken(TokenTypes.EXPR);
166            }
167            else {
168                // case like '@SuppressWarnings(value = UNUSED)'
169                warning = token.findFirstToken(TokenTypes.EXPR);
170            }
171
172            //rare case with empty array ex: @SuppressWarnings({})
173            if (warning == null) {
174                //check to see if empty warnings are forbidden -- are by default
175                logMatch(warningHolder.getLineNo(),
176                    warningHolder.getColumnNo(), "");
177            }
178            else {
179                while (warning != null) {
180                    if (warning.getType() == TokenTypes.EXPR) {
181                        final DetailAST fChild = warning.getFirstChild();
182                        switch (fChild.getType()) {
183                            //typical case
184                            case TokenTypes.STRING_LITERAL:
185                                final String warningText =
186                                    removeQuotes(warning.getFirstChild().getText());
187                                logMatch(warning.getLineNo(),
188                                        warning.getColumnNo(), warningText);
189                                break;
190                            // conditional case
191                            // ex:
192                            // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
193                            case TokenTypes.QUESTION:
194                                walkConditional(fChild);
195                                break;
196                            // param in constant case
197                            // ex: public static final String UNCHECKED = "unchecked";
198                            // @SuppressWarnings(UNCHECKED)
199                            // or
200                            // @SuppressWarnings(SomeClass.UNCHECKED)
201                            case TokenTypes.IDENT:
202                            case TokenTypes.DOT:
203                                break;
204                            default:
205                                // Known limitation: cases like @SuppressWarnings("un" + "used") or
206                                // @SuppressWarnings((String) "unused") are not properly supported,
207                                // but they should not cause exceptions.
208                        }
209                    }
210                    warning = warning.getNextSibling();
211                }
212            }
213        }
214    }
215
216    /**
217     * Gets the {@link SuppressWarnings SuppressWarnings} annotation
218     * that is annotating the AST.  If the annotation does not exist
219     * this method will return {@code null}.
220     *
221     * @param ast the AST
222     * @return the {@link SuppressWarnings SuppressWarnings} annotation
223     */
224    private static DetailAST getSuppressWarnings(DetailAST ast) {
225        final DetailAST annotation = AnnotationUtility.getAnnotation(
226            ast, SUPPRESS_WARNINGS);
227
228        if (annotation == null) {
229            return AnnotationUtility.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
230        }
231        else {
232            return annotation;
233        }
234    }
235
236    /**
237     * This method looks for a warning that matches a configured expression.
238     * If found it logs a violation at the given line and column number.
239     *
240     * @param lineNo the line number
241     * @param colNum the column number
242     * @param warningText the warning.
243     */
244    private void logMatch(final int lineNo,
245        final int colNum, final String warningText) {
246        final Matcher matcher = regexp.matcher(warningText);
247        if (matcher.matches()) {
248            log(lineNo, colNum,
249                    MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
250        }
251    }
252
253    /**
254     * Find the parent (holder) of the of the warnings (Expr).
255     *
256     * @param annotation the annotation
257     * @return a Token representing the expr.
258     */
259    private static DetailAST findWarningsHolder(final DetailAST annotation) {
260        final DetailAST annValuePair =
261            annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
262        final DetailAST annArrayInit;
263
264        if (annValuePair == null) {
265            annArrayInit =
266                    annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
267        }
268        else {
269            annArrayInit =
270                    annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
271        }
272
273        if (annArrayInit != null) {
274            return annArrayInit;
275        }
276
277        return annotation;
278    }
279
280    /**
281     * Strips a single double quote from the front and back of a string.
282     *
283     * <p>For example:
284     * <br/>
285     * Input String = "unchecked"
286     * <br/>
287     * Output String = unchecked
288     *
289     * @param warning the warning string
290     * @return the string without two quotes
291     */
292    private static String removeQuotes(final String warning) {
293        return warning.substring(1, warning.length() - 1);
294    }
295
296    /**
297     * Recursively walks a conditional expression checking the left
298     * and right sides, checking for matches and
299     * logging violations.
300     *
301     * @param cond a Conditional type
302     * {@link TokenTypes#QUESTION QUESTION}
303     */
304    private void walkConditional(final DetailAST cond) {
305        if (cond.getType() == TokenTypes.QUESTION) {
306            walkConditional(getCondLeft(cond));
307            walkConditional(getCondRight(cond));
308        }
309        else {
310            final String warningText =
311                    removeQuotes(cond.getText());
312            logMatch(cond.getLineNo(), cond.getColumnNo(), warningText);
313        }
314    }
315
316    /**
317     * Retrieves the left side of a conditional.
318     *
319     * @param cond cond a conditional type
320     * {@link TokenTypes#QUESTION QUESTION}
321     * @return either the value
322     *     or another conditional
323     */
324    private static DetailAST getCondLeft(final DetailAST cond) {
325        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
326        return colon.getPreviousSibling();
327    }
328
329    /**
330     * Retrieves the right side of a conditional.
331     *
332     * @param cond a conditional type
333     * {@link TokenTypes#QUESTION QUESTION}
334     * @return either the value
335     *     or another conditional
336     */
337    private static DetailAST getCondRight(final DetailAST cond) {
338        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
339        return colon.getNextSibling();
340    }
341}