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;
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.regex.Pattern;
29  
30  import javax.annotation.Nullable;
31  
32  import com.puppycrawl.tools.checkstyle.StatelessCheck;
33  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
35  import com.puppycrawl.tools.checkstyle.api.DetailAST;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  
39  /**
40   * <div>
41   * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations.
42   * It allows to prevent Checkstyle from reporting violations from parts of code that were
43   * annotated with {@code @SuppressWarnings} and using name of the check to be excluded.
44   * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}.
45   * You can also use a {@code checkstyle:} prefix to prevent compiler
46   * from processing these annotations.
47   * You can also define aliases for check names that need to be suppressed.
48   * </div>
49   *
50   * @since 5.7
51   */
52  @StatelessCheck
53  public class SuppressWarningsHolder
54      extends AbstractCheck {
55  
56      /**
57       * Optional prefix for warning suppressions that are only intended to be
58       * recognized by checkstyle. For instance, to suppress {@code
59       * FallThroughCheck} only in checkstyle (and not in javac), use the
60       * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
61       * To suppress the warning in both tools, just use {@code "fallthrough"}.
62       */
63      private static final String CHECKSTYLE_PREFIX = "checkstyle:";
64  
65      /** Java.lang namespace prefix, which is stripped from SuppressWarnings. */
66      private static final String JAVA_LANG_PREFIX = "java.lang.";
67  
68      /** Suffix to be removed from subclasses of Check. */
69      private static final String CHECK_SUFFIX = "check";
70  
71      /** Special warning id for matching all the warnings. */
72      private static final String ALL_WARNING_MATCHING_ID = "all";
73  
74      /** A map from check source names to suppression aliases. */
75      private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
76  
77      /**
78       * A thread-local holder for the list of suppression entries for the last
79       * file parsed.
80       */
81      private static final ThreadLocal<List<Entry>> ENTRIES =
82              ThreadLocal.withInitial(ArrayList::new);
83  
84      /**
85       * Compiled pattern used to match whitespace in text block content.
86       */
87      private static final Pattern WHITESPACE = Pattern.compile("\\s+");
88  
89      /**
90       * Compiled pattern used to match preceding newline in text block content.
91       */
92      private static final Pattern NEWLINE = Pattern.compile("\\n");
93  
94      /**
95       * Creates a new {@code SuppressWarningsHolder} instance.
96       */
97      public SuppressWarningsHolder() {
98          // no code by default
99      }
100 
101     /**
102      * Returns the default alias for the source name of a check, which is the
103      * source name in lower case with any dotted prefix or "Check"/"check"
104      * suffix removed.
105      *
106      * @param sourceName the source name of the check (generally the class
107      *        name)
108      * @return the default alias for the given check
109      */
110     public static String getDefaultAlias(String sourceName) {
111         int endIndex = sourceName.length();
112         final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH);
113         if (sourceNameLower.endsWith(CHECK_SUFFIX)) {
114             endIndex -= CHECK_SUFFIX.length();
115         }
116         final int startIndex = sourceNameLower.lastIndexOf('.') + 1;
117         return sourceNameLower.substring(startIndex, endIndex);
118     }
119 
120     /**
121      * Returns the alias of simple check name for a check, The alias is
122      * for the form of CheckNameCheck or CheckName.
123      *
124      * @param sourceName the source name of the check (generally the class
125      *        name)
126      * @return the alias of the simple check name for the given check
127      */
128     @Nullable
129     private static String getSimpleNameAlias(String sourceName) {
130         final String checkName = CommonUtil.baseClassName(sourceName);
131         final String checkNameSuffix = "Check";
132         // check alias for the CheckNameCheck
133         String checkAlias = CHECK_ALIAS_MAP.get(checkName);
134         if (checkAlias == null && checkName.endsWith(checkNameSuffix)) {
135             final int checkStartIndex = checkName.length() - checkNameSuffix.length();
136             final String checkNameWithoutSuffix = checkName.substring(0, checkStartIndex);
137             // check alias for the CheckName
138             checkAlias = CHECK_ALIAS_MAP.get(checkNameWithoutSuffix);
139         }
140 
141         return checkAlias;
142     }
143 
144     /**
145      * Returns the alias for the source name of a check. If an alias has been
146      * explicitly registered via {@link #setAliasList(String...)}, that
147      * alias is returned; otherwise, the default alias is used.
148      *
149      * @param sourceName the source name of the check (generally the class
150      *        name)
151      * @return the current alias for the given check
152      */
153     public static String getAlias(String sourceName) {
154         String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
155         if (checkAlias == null) {
156             checkAlias = getSimpleNameAlias(sourceName);
157         }
158         if (checkAlias == null) {
159             checkAlias = getDefaultAlias(sourceName);
160         }
161         return checkAlias;
162     }
163 
164     /**
165      * Registers an alias for the source name of a check.
166      *
167      * @param sourceName the source name of the check (generally the class
168      *        name)
169      * @param checkAlias the alias used in {@link SuppressWarnings} annotations
170      */
171     private static void registerAlias(String sourceName, String checkAlias) {
172         CHECK_ALIAS_MAP.put(sourceName, checkAlias);
173     }
174 
175     /**
176      * Setter to specify aliases for check names that can be used in code
177      * within {@code SuppressWarnings} in a format of comma separated attribute=value entries.
178      * The attribute is the fully qualified name of the Check and value is its alias.
179      *
180      * @param aliasList comma-separated alias assignments
181      * @throws IllegalArgumentException when alias item does not have '='
182      * @since 5.7
183      */
184     public void setAliasList(String... aliasList) {
185         for (String sourceAlias : aliasList) {
186             final int index = sourceAlias.indexOf('=');
187             if (index > 0) {
188                 registerAlias(sourceAlias.substring(0, index), sourceAlias
189                     .substring(index + 1));
190             }
191             else if (!sourceAlias.isEmpty()) {
192                 throw new IllegalArgumentException(
193                     "'=' expected in alias list item: " + sourceAlias);
194             }
195         }
196     }
197 
198     /**
199      * Checks for a suppression of a check with the given source name and
200      * location in the last file processed.
201      *
202      * @param event audit event.
203      * @return whether the check with the given name is suppressed at the given
204      *         source location
205      */
206     public static boolean isSuppressed(AuditEvent event) {
207         final List<Entry> entries = ENTRIES.get();
208         final String sourceName = event.getSourceName();
209         final String checkAlias = getAlias(sourceName);
210         final int line = event.getLine();
211         final int column = event.getColumn();
212         boolean suppressed = false;
213         for (Entry entry : entries) {
214             final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
215             final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
216             final String checkName = entry.checkName();
217             final boolean nameMatches =
218                 ALL_WARNING_MATCHING_ID.equals(checkName)
219                     || checkName.equalsIgnoreCase(checkAlias)
220                     || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias)
221                     || getDefaultAlias(sourceName).equalsIgnoreCase(checkName);
222             if (afterStart && beforeEnd
223                     && (nameMatches || checkName.equals(event.getModuleId()))) {
224                 suppressed = true;
225                 break;
226             }
227         }
228         return suppressed;
229     }
230 
231     /**
232      * Checks whether suppression entry position is after the audit event occurrence position
233      * in the source file.
234      *
235      * @param line the line number in the source file where the event occurred.
236      * @param column the column number in the source file where the event occurred.
237      * @param entry suppression entry.
238      * @return true if suppression entry position is after the audit event occurrence position
239      *         in the source file.
240      */
241     private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
242         return entry.firstLine() < line
243             || entry.firstLine() == line
244             && (column == 0 || entry.firstColumn() <= column);
245     }
246 
247     /**
248      * Checks whether suppression entry position is before the audit event occurrence position
249      * in the source file.
250      *
251      * @param line the line number in the source file where the event occurred.
252      * @param column the column number in the source file where the event occurred.
253      * @param entry suppression entry.
254      * @return true if suppression entry position is before the audit event occurrence position
255      *         in the source file.
256      */
257     private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
258         return entry.lastLine() > line
259             || entry.lastLine() == line && entry
260                 .lastColumn() >= column;
261     }
262 
263     @Override
264     public int[] getDefaultTokens() {
265         return getRequiredTokens();
266     }
267 
268     @Override
269     public int[] getAcceptableTokens() {
270         return getRequiredTokens();
271     }
272 
273     @Override
274     public int[] getRequiredTokens() {
275         return new int[] {TokenTypes.ANNOTATION};
276     }
277 
278     @Override
279     public void beginTree(DetailAST rootAST) {
280         ENTRIES.get().clear();
281     }
282 
283     @Override
284     public void visitToken(DetailAST ast) {
285         // check whether annotation is SuppressWarnings
286         // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
287         String identifier = getIdentifier(getNthChild(ast, 1));
288         if (identifier.startsWith(JAVA_LANG_PREFIX)) {
289             identifier = identifier.substring(JAVA_LANG_PREFIX.length());
290         }
291         if ("SuppressWarnings".equals(identifier)) {
292             getAnnotationTarget(ast).ifPresent(targetAST -> {
293                 addSuppressions(getAllAnnotationValues(ast), targetAST);
294             });
295         }
296     }
297 
298     /**
299      * Method to populate list of suppression entries.
300      *
301      * @param values
302      *            - list of check names
303      * @param targetAST
304      *            - annotation target
305      */
306     private static void addSuppressions(List<String> values, DetailAST targetAST) {
307         // get text range of target
308         final int firstLine = targetAST.getLineNo();
309         final int firstColumn = targetAST.getColumnNo();
310         final DetailAST nextAST = targetAST.getNextSibling();
311         final int lastLine;
312         final int lastColumn;
313         if (nextAST == null) {
314             lastLine = Integer.MAX_VALUE;
315             lastColumn = Integer.MAX_VALUE;
316         }
317         else {
318             lastLine = nextAST.getLineNo();
319             lastColumn = nextAST.getColumnNo();
320         }
321 
322         final List<Entry> entries = ENTRIES.get();
323         for (String value : values) {
324             // strip off the checkstyle-only prefix if present
325             final String checkName = removeCheckstylePrefixIfExists(value);
326             entries.add(new Entry(checkName, firstLine, firstColumn,
327                     lastLine, lastColumn));
328         }
329     }
330 
331     /**
332      * Method removes checkstyle prefix (checkstyle:) from check name if exists.
333      *
334      * @param checkName
335      *            - name of the check
336      * @return check name without prefix
337      */
338     private static String removeCheckstylePrefixIfExists(String checkName) {
339         String result = checkName;
340         if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
341             result = checkName.substring(CHECKSTYLE_PREFIX.length());
342         }
343         return result;
344     }
345 
346     /**
347      * Get all annotation values.
348      *
349      * @param ast annotation token
350      * @return list values
351      * @throws IllegalArgumentException if there is an unknown annotation value type.
352      */
353     private static List<String> getAllAnnotationValues(DetailAST ast) {
354         // get values of annotation
355         List<String> values = List.of();
356         final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
357         if (lparenAST != null) {
358             final DetailAST nextAST = lparenAST.getNextSibling();
359             final int nextType = nextAST.getType();
360             switch (nextType) {
361                 case TokenTypes.EXPR, TokenTypes.ANNOTATION_ARRAY_INIT ->
362                     values = getAnnotationValues(nextAST);
363                 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR ->
364                     // expected children: IDENT ASSIGN ( EXPR |
365                     // ANNOTATION_ARRAY_INIT )
366                     values = getAnnotationValues(getNthChild(nextAST, 2));
367                 case TokenTypes.RPAREN -> {
368                     // no value present (not valid Java)
369                 }
370                 default ->
371                     // unknown annotation value type (new syntax?)
372                     throw new IllegalArgumentException("Unexpected AST: " + nextAST);
373             }
374         }
375         return values;
376     }
377 
378     /**
379      * Get target of annotation.
380      *
381      * @param ast the AST node to get the child of
382      * @return get target of annotation
383      * @throws IllegalArgumentException if there is an unexpected container type.
384      */
385     private static Optional<DetailAST> getAnnotationTarget(DetailAST ast) {
386         DetailAST current = ast.getParent();
387         while (current.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
388             current = current.getParent();
389         }
390         return switch (current.getType()) {
391             case TokenTypes.MODIFIERS, TokenTypes.ANNOTATIONS, TokenTypes.ANNOTATION,
392                  TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR -> Optional.of(current.getParent());
393             case TokenTypes.LITERAL_DEFAULT -> Optional.empty();
394             default -> throw new IllegalArgumentException("Unexpected container AST: " + current);
395         };
396     }
397 
398     /**
399      * Returns the n'th child of an AST node.
400      *
401      * @param ast the AST node to get the child of
402      * @param index the index of the child to get
403      * @return the n'th child of the given AST node, or {@code null} if none
404      */
405     private static DetailAST getNthChild(DetailAST ast, int index) {
406         DetailAST child = ast.getFirstChild();
407         for (int i = 0; i < index && child != null; i++) {
408             child = child.getNextSibling();
409         }
410         return child;
411     }
412 
413     /**
414      * Returns the Java identifier represented by an AST.
415      *
416      * @param ast an AST node for an IDENT or DOT
417      * @return the Java identifier represented by the given AST subtree
418      * @throws IllegalArgumentException if the AST is invalid
419      */
420     private static String getIdentifier(DetailAST ast) {
421         if (ast == null) {
422             throw new IllegalArgumentException("Identifier AST expected, but get null.");
423         }
424         final String identifier;
425         if (ast.getType() == TokenTypes.IDENT) {
426             identifier = ast.getText();
427         }
428         else {
429             identifier = getIdentifier(ast.getFirstChild()) + "."
430                 + getIdentifier(ast.getLastChild());
431         }
432         return identifier;
433     }
434 
435     /**
436      * Returns the literal string expression represented by an AST.
437      *
438      * @param ast an AST node for an EXPR
439      * @return the Java string represented by the given AST expression
440      *         or empty string if expression is too complex
441      * @throws IllegalArgumentException if the AST is invalid
442      */
443     private static String getStringExpr(DetailAST ast) {
444         final DetailAST firstChild = ast.getFirstChild();
445 
446         return switch (firstChild.getType()) {
447             case TokenTypes.STRING_LITERAL -> {
448                 // NOTE: escaped characters are not unescaped
449                 final String quotedText = firstChild.getText();
450                 yield quotedText.substring(1, quotedText.length() - 1);
451             }
452             case TokenTypes.IDENT -> firstChild.getText();
453             case TokenTypes.DOT -> firstChild.getLastChild().getText();
454             case TokenTypes.TEXT_BLOCK_LITERAL_BEGIN -> {
455                 final String textBlockContent = firstChild.getFirstChild().getText();
456                 yield getContentWithoutPrecedingWhitespace(textBlockContent);
457             }
458             default ->
459                 // annotations with complex expressions cannot suppress warnings
460                 "";
461         };
462     }
463 
464     /**
465      * Returns the annotation values represented by an AST.
466      *
467      * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
468      * @return the list of Java string represented by the given AST for an
469      *         expression or annotation array initializer
470      * @throws IllegalArgumentException if the AST is invalid
471      */
472     private static List<String> getAnnotationValues(DetailAST ast) {
473         return switch (ast.getType()) {
474             case TokenTypes.EXPR -> List.of(getStringExpr(ast));
475             case TokenTypes.ANNOTATION_ARRAY_INIT -> findAllExpressionsInChildren(ast);
476             default -> throw new IllegalArgumentException(
477                     "Expression or annotation array initializer AST expected: " + ast);
478         };
479     }
480 
481     /**
482      * Method looks at children and returns list of expressions in strings.
483      *
484      * @param parent ast, that contains children
485      * @return list of expressions in strings
486      */
487     private static List<String> findAllExpressionsInChildren(DetailAST parent) {
488         final List<String> valueList = new ArrayList<>();
489         DetailAST childAST = parent.getFirstChild();
490         while (childAST != null) {
491             if (childAST.getType() == TokenTypes.EXPR) {
492                 valueList.add(getStringExpr(childAST));
493             }
494             childAST = childAST.getNextSibling();
495         }
496         return valueList;
497     }
498 
499     /**
500      * Remove preceding newline and whitespace from the content of a text block.
501      *
502      * @param textBlockContent the actual text in a text block.
503      * @return content of text block with preceding whitespace and newline removed.
504      */
505     private static String getContentWithoutPrecedingWhitespace(String textBlockContent) {
506         final String contentWithNoPrecedingNewline =
507             NEWLINE.matcher(textBlockContent).replaceAll("");
508         return WHITESPACE.matcher(contentWithNoPrecedingNewline).replaceAll("");
509     }
510 
511     @Override
512     public void destroy() {
513         super.destroy();
514         ENTRIES.remove();
515     }
516 
517     /**
518      * Records a particular suppression for a region of a file.
519      *
520      * @param checkName   the source name of the suppressed check
521      * @param firstLine   the first line of the suppression region
522      * @param firstColumn the first column of the suppression region
523      * @param lastLine    the last line of the suppression region
524      * @param lastColumn  the last column of the suppression region
525      */
526     private record Entry(String checkName, int firstLine,
527                          int firstColumn, int lastLine, int lastColumn) {
528 
529     }
530 
531 }