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