001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 *
078 * @since 5.0
079 */
080@StatelessCheck
081public class SuppressWarningsCheck extends AbstractCheck {
082
083    /**
084     * A key is pointing to the warning message text in "messages.properties"
085     * file.
086     */
087    public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
088        "suppressed.warning.not.allowed";
089
090    /** {@link SuppressWarnings SuppressWarnings} annotation name. */
091    private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
092
093    /**
094     * Fully-qualified {@link SuppressWarnings SuppressWarnings}
095     * annotation name.
096     */
097    private static final String FQ_SUPPRESS_WARNINGS =
098        "java.lang." + SUPPRESS_WARNINGS;
099
100    /**
101     * Specify the RegExp to match against warnings. Any warning
102     * being suppressed matching this pattern will be flagged.
103     */
104    private Pattern format = Pattern.compile("^\\s*+$");
105
106    /**
107     * Creates a new {@code SuppressWarningsCheck} instance.
108     */
109    public SuppressWarningsCheck() {
110        // no code by default
111    }
112
113    /**
114     * Setter to specify the RegExp to match against warnings. Any warning
115     * being suppressed matching this pattern will be flagged.
116     *
117     * @param pattern the new pattern
118     * @since 5.0
119     */
120    public final void setFormat(Pattern pattern) {
121        format = pattern;
122    }
123
124    @Override
125    public final int[] getDefaultTokens() {
126        return getAcceptableTokens();
127    }
128
129    @Override
130    public final int[] getAcceptableTokens() {
131        return new int[] {
132            TokenTypes.CLASS_DEF,
133            TokenTypes.INTERFACE_DEF,
134            TokenTypes.ENUM_DEF,
135            TokenTypes.ANNOTATION_DEF,
136            TokenTypes.ANNOTATION_FIELD_DEF,
137            TokenTypes.ENUM_CONSTANT_DEF,
138            TokenTypes.PARAMETER_DEF,
139            TokenTypes.VARIABLE_DEF,
140            TokenTypes.METHOD_DEF,
141            TokenTypes.CTOR_DEF,
142            TokenTypes.COMPACT_CTOR_DEF,
143            TokenTypes.RECORD_DEF,
144            TokenTypes.PATTERN_VARIABLE_DEF,
145            TokenTypes.MODULE_DEF,
146        };
147    }
148
149    @Override
150    public int[] getRequiredTokens() {
151        return CommonUtil.EMPTY_INT_ARRAY;
152    }
153
154    @Override
155    public void visitToken(final DetailAST ast) {
156        final DetailAST annotation = getSuppressWarnings(ast);
157
158        if (annotation != null) {
159            final DetailAST warningHolder =
160                findWarningsHolder(annotation);
161            final DetailAST token =
162                    warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
163
164            // case like '@SuppressWarnings(value = UNUSED)'
165            final DetailAST parent = Objects.requireNonNullElse(token, warningHolder);
166            final DetailAST warning = parent.findFirstToken(TokenTypes.EXPR);
167
168            if (warning == null) {
169                // check to see if empty warnings are forbidden -- are by default
170                logMatch(warningHolder, "");
171            }
172            else {
173                processWarnings(warning);
174            }
175        }
176    }
177
178    /**
179     * Processes all warning expressions starting from the given AST node.
180     *
181     * @param warning the first warning expression node to process
182     */
183    private void processWarnings(final DetailAST warning) {
184        for (DetailAST current = warning; current != null; current = current.getNextSibling()) {
185            if (current.getType() == TokenTypes.EXPR) {
186                processWarningExpr(current.getFirstChild(), current);
187            }
188        }
189    }
190
191    /**
192     * Processes a single warning expression.
193     *
194     * @param firstChild  the first child AST of the expression
195     * @param warning the parent warning AST node
196     */
197    private void processWarningExpr(final DetailAST firstChild, final DetailAST warning) {
198        switch (firstChild.getType()) {
199            case TokenTypes.STRING_LITERAL -> logMatch(warning,
200                    removeQuotes(warning.getFirstChild().getText()));
201
202            case TokenTypes.QUESTION ->
203                // ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
204                walkConditional(firstChild);
205
206            default -> {
207            // Known limitation: cases like @SuppressWarnings("un" + "used") or
208            // @SuppressWarnings((String) "unused") are not properly supported,
209            // but they should not cause exceptions.
210            // Also constants as params:
211            // ex: public static final String UNCHECKED = "unchecked";
212            // @SuppressWarnings(UNCHECKED)
213            // or
214            // @SuppressWarnings(SomeClass.UNCHECKED)
215            }
216        }
217    }
218
219    /**
220     * Gets the {@link SuppressWarnings SuppressWarnings} annotation
221     * that is annotating the AST.  If the annotation does not exist
222     * this method will return {@code null}.
223     *
224     * @param ast the AST
225     * @return the {@code SuppressWarnings SuppressWarnings} annotation
226     */
227    private static DetailAST getSuppressWarnings(DetailAST ast) {
228        DetailAST annotation = AnnotationUtil.getAnnotation(ast, SUPPRESS_WARNINGS);
229
230        if (annotation == null) {
231            annotation = AnnotationUtil.getAnnotation(ast, FQ_SUPPRESS_WARNINGS);
232        }
233        return annotation;
234    }
235
236    /**
237     * This method looks for a warning that matches a configured expression.
238     * If found it logs a violation at the given AST.
239     *
240     * @param ast the location to place the violation
241     * @param warningText the warning.
242     */
243    private void logMatch(DetailAST ast, final String warningText) {
244        final Matcher matcher = format.matcher(warningText);
245        if (matcher.matches()) {
246            log(ast,
247                    MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
248        }
249    }
250
251    /**
252     * Find the parent (holder) of the of the warnings (Expr).
253     *
254     * @param annotation the annotation
255     * @return a Token representing the expr.
256     */
257    private static DetailAST findWarningsHolder(final DetailAST annotation) {
258        final DetailAST annValuePair =
259            annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
260
261        final DetailAST annArrayInitParent = Objects.requireNonNullElse(annValuePair, annotation);
262        final DetailAST annArrayInit = annArrayInitParent
263                .findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
264        return Objects.requireNonNullElse(annArrayInit, annotation);
265    }
266
267    /**
268     * Strips a single double quote from the front and back of a string.
269     *
270     * <p>For example:</p>
271     * {@snippet lang="text" :
272     *     Input String = "unchecked"
273     * }
274     * Output String = unchecked
275     *
276     * @param warning the warning string
277     * @return the string without two quotes
278     */
279    private static String removeQuotes(final String warning) {
280        return warning.substring(1, warning.length() - 1);
281    }
282
283    /**
284     * Walks a conditional expression checking the left
285     * and right sides, checking for matches and
286     * logging violations.
287     *
288     * @param cond a Conditional type
289     *     {@link TokenTypes#QUESTION QUESTION}
290     */
291    private void walkConditional(final DetailAST cond) {
292        final Deque<DetailAST> condStack = new ArrayDeque<>();
293        condStack.push(cond);
294
295        while (!condStack.isEmpty()) {
296            final DetailAST currentCond = condStack.pop();
297            if (currentCond.getType() == TokenTypes.QUESTION) {
298                condStack.push(getCondRight(currentCond));
299                condStack.push(getCondLeft(currentCond));
300            }
301            else {
302                final String warningText = removeQuotes(currentCond.getText());
303                logMatch(currentCond, warningText);
304            }
305        }
306    }
307
308    /**
309     * Retrieves the left side of a conditional.
310     *
311     * @param cond cond a conditional type
312     *     {@link TokenTypes#QUESTION QUESTION}
313     * @return either the value
314     *     or another conditional
315     */
316    private static DetailAST getCondLeft(final DetailAST cond) {
317        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
318        return colon.getPreviousSibling();
319    }
320
321    /**
322     * Retrieves the right side of a conditional.
323     *
324     * @param cond a conditional type
325     *     {@link TokenTypes#QUESTION QUESTION}
326     * @return either the value
327     *     or another conditional
328     */
329    private static DetailAST getCondRight(final DetailAST cond) {
330        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
331        return colon.getNextSibling();
332    }
333
334}