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