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