View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.annotation;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.Objects;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import com.puppycrawl.tools.checkstyle.StatelessCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
34  
35  /**
36   * <div>
37   * Allows to specify what warnings that
38   * {@code @SuppressWarnings} is not allowed to suppress.
39   * You can also specify a list of TokenTypes that
40   * the configured warning(s) cannot be suppressed on.
41   * </div>
42   *
43   * <p>
44   * Limitations:  This check does not consider conditionals
45   * inside the &#64;SuppressWarnings annotation.
46   * </p>
47   *
48   * <p>
49   * For example:
50   * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}.
51   * According to the above example, the "unused" warning is being suppressed
52   * not the "unchecked" or "foo" warnings.  All of these warnings will be
53   * considered and matched against regardless of what the conditional
54   * evaluates to.
55   * The check also does not support code like {@code @SuppressWarnings("un" + "used")},
56   * {@code @SuppressWarnings((String) "unused")} or
57   * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}.
58   * </p>
59   *
60   * <p>
61   * By default, any warning specified will be disallowed on
62   * all legal TokenTypes unless otherwise specified via
63   * the tokens property.
64   * </p>
65   *
66   * <p>
67   * Also, by default warnings that are empty strings or all
68   * whitespace (regex: ^$|^\s+$) are flagged.  By specifying,
69   * the format property these defaults no longer apply.
70   * </p>
71   *
72   * <p>This check can be configured so that the "unchecked"
73   * and "unused" warnings cannot be suppressed on
74   * anything but variable and parameter declarations.
75   * See below of an example.
76   * </p>
77   *
78   * @since 5.0
79   */
80  @StatelessCheck
81  public class SuppressWarningsCheck extends AbstractCheck {
82  
83      /**
84       * A key is pointing to the warning message text in "messages.properties"
85       * file.
86       */
87      public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
88          "suppressed.warning.not.allowed";
89  
90      /** {@link SuppressWarnings SuppressWarnings} annotation name. */
91      private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
92  
93      /**
94       * Fully-qualified {@link SuppressWarnings SuppressWarnings}
95       * annotation name.
96       */
97      private static final String FQ_SUPPRESS_WARNINGS =
98          "java.lang." + SUPPRESS_WARNINGS;
99  
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 }