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