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