001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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 * Type is {@code java.util.regex.Pattern}.
074 * Default value is {@code "^\s*+$"}.
075 * </li>
076 * <li>
077 * Property {@code tokens} - tokens to check
078 * Type is {@code java.lang.String[]}.
079 * Validation type is {@code tokenSet}.
080 * Default value is:
081 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
082 * CLASS_DEF</a>,
083 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
084 * INTERFACE_DEF</a>,
085 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
086 * ENUM_DEF</a>,
087 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
088 * ANNOTATION_DEF</a>,
089 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
090 * ANNOTATION_FIELD_DEF</a>,
091 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
092 * ENUM_CONSTANT_DEF</a>,
093 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF">
094 * PARAMETER_DEF</a>,
095 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
096 * VARIABLE_DEF</a>,
097 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
098 * METHOD_DEF</a>,
099 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
100 * CTOR_DEF</a>,
101 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
102 * COMPACT_CTOR_DEF</a>,
103 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
104 * RECORD_DEF</a>.
105 * </li>
106 * </ul>
107 * <p>
108 * To configure the check:
109 * </p>
110 * <pre>
111 * &lt;module name=&quot;SuppressWarnings&quot;/&gt;
112 * </pre>
113 * <p>
114 * To configure the check so that the "unchecked" and "unused"
115 * warnings cannot be suppressed on anything but variable and parameter declarations.
116 * </p>
117 * <pre>
118 * &lt;module name=&quot;SuppressWarnings&quot;&gt;
119 *   &lt;property name=&quot;format&quot;
120 *       value=&quot;^unchecked$|^unused$&quot;/&gt;
121 *   &lt;property name=&quot;tokens&quot;
122 *     value=&quot;
123 *     CLASS_DEF,INTERFACE_DEF,ENUM_DEF,
124 *     ANNOTATION_DEF,ANNOTATION_FIELD_DEF,
125 *     ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF
126 *     &quot;/&gt;
127 * &lt;/module&gt;
128 * </pre>
129 * <p>
130 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
131 * </p>
132 * <p>
133 * Violation Message Keys:
134 * </p>
135 * <ul>
136 * <li>
137 * {@code suppressed.warning.not.allowed}
138 * </li>
139 * </ul>
140 *
141 * @since 5.0
142 */
143@StatelessCheck
144public class SuppressWarningsCheck extends AbstractCheck {
145
146    /**
147     * A key is pointing to the warning message text in "messages.properties"
148     * file.
149     */
150    public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
151        "suppressed.warning.not.allowed";
152
153    /** {@link SuppressWarnings SuppressWarnings} annotation name. */
154    private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
155
156    /**
157     * Fully-qualified {@link SuppressWarnings SuppressWarnings}
158     * annotation name.
159     */
160    private static final String FQ_SUPPRESS_WARNINGS =
161        "java.lang." + SUPPRESS_WARNINGS;
162
163    /**
164     * Specify the RegExp to match against warnings. Any warning
165     * being suppressed matching this pattern will be flagged.
166     */
167    private Pattern format = Pattern.compile("^\\s*+$");
168
169    /**
170     * Setter to specify the RegExp to match against warnings. Any warning
171     * being suppressed matching this pattern will be flagged.
172     *
173     * @param pattern the new pattern
174     */
175    public final void setFormat(Pattern pattern) {
176        format = pattern;
177    }
178
179    @Override
180    public final int[] getDefaultTokens() {
181        return getAcceptableTokens();
182    }
183
184    @Override
185    public final int[] getAcceptableTokens() {
186        return new int[] {
187            TokenTypes.CLASS_DEF,
188            TokenTypes.INTERFACE_DEF,
189            TokenTypes.ENUM_DEF,
190            TokenTypes.ANNOTATION_DEF,
191            TokenTypes.ANNOTATION_FIELD_DEF,
192            TokenTypes.ENUM_CONSTANT_DEF,
193            TokenTypes.PARAMETER_DEF,
194            TokenTypes.VARIABLE_DEF,
195            TokenTypes.METHOD_DEF,
196            TokenTypes.CTOR_DEF,
197            TokenTypes.COMPACT_CTOR_DEF,
198            TokenTypes.RECORD_DEF,
199        };
200    }
201
202    @Override
203    public int[] getRequiredTokens() {
204        return CommonUtil.EMPTY_INT_ARRAY;
205    }
206
207    @Override
208    public void visitToken(final DetailAST ast) {
209        final DetailAST annotation = getSuppressWarnings(ast);
210
211        if (annotation != null) {
212            final DetailAST warningHolder =
213                findWarningsHolder(annotation);
214
215            final DetailAST token =
216                    warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
217            DetailAST warning;
218
219            if (token == null) {
220                warning = warningHolder.findFirstToken(TokenTypes.EXPR);
221            }
222            else {
223                // case like '@SuppressWarnings(value = UNUSED)'
224                warning = token.findFirstToken(TokenTypes.EXPR);
225            }
226
227            // rare case with empty array ex: @SuppressWarnings({})
228            if (warning == null) {
229                // check to see if empty warnings are forbidden -- are by default
230                logMatch(warningHolder, "");
231            }
232            else {
233                while (warning != null) {
234                    if (warning.getType() == TokenTypes.EXPR) {
235                        final DetailAST fChild = warning.getFirstChild();
236                        switch (fChild.getType()) {
237                            // typical case
238                            case TokenTypes.STRING_LITERAL:
239                                final String warningText =
240                                    removeQuotes(warning.getFirstChild().getText());
241                                logMatch(warning, warningText);
242                                break;
243                            // conditional case
244                            // ex:
245                            // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
246                            case TokenTypes.QUESTION:
247                                walkConditional(fChild);
248                                break;
249                            // param in constant case
250                            // ex: public static final String UNCHECKED = "unchecked";
251                            // @SuppressWarnings(UNCHECKED)
252                            // or
253                            // @SuppressWarnings(SomeClass.UNCHECKED)
254                            case TokenTypes.IDENT:
255                            case TokenTypes.DOT:
256                                break;
257                            default:
258                                // Known limitation: cases like @SuppressWarnings("un" + "used") or
259                                // @SuppressWarnings((String) "unused") are not properly supported,
260                                // but they should not cause exceptions.
261                        }
262                    }
263                    warning = warning.getNextSibling();
264                }
265            }
266        }
267    }
268
269    /**
270     * Gets the {@link SuppressWarnings SuppressWarnings} annotation
271     * that is annotating the AST.  If the annotation does not exist
272     * this method will return {@code null}.
273     *
274     * @param ast the AST
275     * @return the {@link SuppressWarnings SuppressWarnings} annotation
276     */
277    private static DetailAST getSuppressWarnings(DetailAST ast) {
278        DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS);
279
280        if (annotation == null) {
281            annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
282        }
283        return annotation;
284    }
285
286    /**
287     * This method looks for a warning that matches a configured expression.
288     * If found it logs a violation at the given AST.
289     *
290     * @param ast the location to place the violation
291     * @param warningText the warning.
292     */
293    private void logMatch(DetailAST ast, final String warningText) {
294        final Matcher matcher = format.matcher(warningText);
295        if (matcher.matches()) {
296            log(ast,
297                    MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
298        }
299    }
300
301    /**
302     * Find the parent (holder) of the of the warnings (Expr).
303     *
304     * @param annotation the annotation
305     * @return a Token representing the expr.
306     */
307    private static DetailAST findWarningsHolder(final DetailAST annotation) {
308        final DetailAST annValuePair =
309            annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
310        final DetailAST annArrayInit;
311
312        if (annValuePair == null) {
313            annArrayInit =
314                    annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
315        }
316        else {
317            annArrayInit =
318                    annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
319        }
320
321        DetailAST warningsHolder = annotation;
322        if (annArrayInit != null) {
323            warningsHolder = annArrayInit;
324        }
325
326        return warningsHolder;
327    }
328
329    /**
330     * Strips a single double quote from the front and back of a string.
331     *
332     * <p>For example:
333     * <br/>
334     * Input String = "unchecked"
335     * <br/>
336     * Output String = unchecked
337     *
338     * @param warning the warning string
339     * @return the string without two quotes
340     */
341    private static String removeQuotes(final String warning) {
342        return warning.substring(1, warning.length() - 1);
343    }
344
345    /**
346     * Recursively walks a conditional expression checking the left
347     * and right sides, checking for matches and
348     * logging violations.
349     *
350     * @param cond a Conditional type
351     *     {@link TokenTypes#QUESTION QUESTION}
352     */
353    private void walkConditional(final DetailAST cond) {
354        if (cond.getType() == TokenTypes.QUESTION) {
355            walkConditional(getCondLeft(cond));
356            walkConditional(getCondRight(cond));
357        }
358        else {
359            final String warningText =
360                    removeQuotes(cond.getText());
361            logMatch(cond, warningText);
362        }
363    }
364
365    /**
366     * Retrieves the left side of a conditional.
367     *
368     * @param cond cond a conditional type
369     *     {@link TokenTypes#QUESTION QUESTION}
370     * @return either the value
371     *     or another conditional
372     */
373    private static DetailAST getCondLeft(final DetailAST cond) {
374        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
375        return colon.getPreviousSibling();
376    }
377
378    /**
379     * Retrieves the right side of a conditional.
380     *
381     * @param cond a conditional type
382     *     {@link TokenTypes#QUESTION QUESTION}
383     * @return either the value
384     *     or another conditional
385     */
386    private static DetailAST getCondRight(final DetailAST cond) {
387        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
388        return colon.getNextSibling();
389    }
390
391}