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