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