View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.Collections;
23  import java.util.HashMap;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.regex.Pattern;
30  
31  import javax.annotation.Nullable;
32  
33  import com.puppycrawl.tools.checkstyle.StatelessCheck;
34  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
35  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
36  import com.puppycrawl.tools.checkstyle.api.DetailAST;
37  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39  
40  /**
41   * <div>
42   * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations.
43   * It allows to prevent Checkstyle from reporting violations from parts of code that were
44   * annotated with {@code @SuppressWarnings} and using name of the check to be excluded.
45   * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}.
46   * You can also use a {@code checkstyle:} prefix to prevent compiler
47   * from processing these annotations.
48   * You can also define aliases for check names that need to be suppressed.
49   * </div>
50   *
51   * @since 5.7
52   */
53  @StatelessCheck
54  public class SuppressWarningsHolder
55      extends AbstractCheck {
56  
57      /**
58       * Optional prefix for warning suppressions that are only intended to be
59       * recognized by checkstyle. For instance, to suppress {@code
60       * FallThroughCheck} only in checkstyle (and not in javac), use the
61       * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
62       * To suppress the warning in both tools, just use {@code "fallthrough"}.
63       */
64      private static final String CHECKSTYLE_PREFIX = "checkstyle:";
65  
66      /** Java.lang namespace prefix, which is stripped from SuppressWarnings. */
67      private static final String JAVA_LANG_PREFIX = "java.lang.";
68  
69      /** Suffix to be removed from subclasses of Check. */
70      private static final String CHECK_SUFFIX = "check";
71  
72      /** Special warning id for matching all the warnings. */
73      private static final String ALL_WARNING_MATCHING_ID = "all";
74  
75      /** A map from check source names to suppression aliases. */
76      private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
77  
78      /**
79       * A thread-local holder for the list of suppression entries for the last
80       * file parsed.
81       */
82      private static final ThreadLocal<List<Entry>> ENTRIES =
83              ThreadLocal.withInitial(LinkedList::new);
84  
85      /**
86       * Compiled pattern used to match whitespace in text block content.
87       */
88      private static final Pattern WHITESPACE = Pattern.compile("\\s+");
89  
90      /**
91       * Compiled pattern used to match preceding newline in text block content.
92       */
93      private static final Pattern NEWLINE = Pattern.compile("\\n");
94  
95      /**
96       * Returns the default alias for the source name of a check, which is the
97       * source name in lower case with any dotted prefix or "Check"/"check"
98       * suffix removed.
99       *
100      * @param sourceName the source name of the check (generally the class
101      *        name)
102      * @return the default alias for the given check
103      */
104     public static String getDefaultAlias(String sourceName) {
105         int endIndex = sourceName.length();
106         final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH);
107         if (sourceNameLower.endsWith(CHECK_SUFFIX)) {
108             endIndex -= CHECK_SUFFIX.length();
109         }
110         final int startIndex = sourceNameLower.lastIndexOf('.') + 1;
111         return sourceNameLower.substring(startIndex, endIndex);
112     }
113 
114     /**
115      * Returns the alias of simple check name for a check, The alias is
116      * for the form of CheckNameCheck or CheckName.
117      *
118      * @param sourceName the source name of the check (generally the class
119      *        name)
120      * @return the alias of the simple check name for the given check
121      */
122     @Nullable
123     private static String getSimpleNameAlias(String sourceName) {
124         final String checkName = CommonUtil.baseClassName(sourceName);
125         final String checkNameSuffix = "Check";
126         // check alias for the CheckNameCheck
127         String checkAlias = CHECK_ALIAS_MAP.get(checkName);
128         if (checkAlias == null && checkName.endsWith(checkNameSuffix)) {
129             final int checkStartIndex = checkName.length() - checkNameSuffix.length();
130             final String checkNameWithoutSuffix = checkName.substring(0, checkStartIndex);
131             // check alias for the CheckName
132             checkAlias = CHECK_ALIAS_MAP.get(checkNameWithoutSuffix);
133         }
134 
135         return checkAlias;
136     }
137 
138     /**
139      * Returns the alias for the source name of a check. If an alias has been
140      * explicitly registered via {@link #setAliasList(String...)}, that
141      * alias is returned; otherwise, the default alias is used.
142      *
143      * @param sourceName the source name of the check (generally the class
144      *        name)
145      * @return the current alias for the given check
146      */
147     public static String getAlias(String sourceName) {
148         String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
149         if (checkAlias == null) {
150             checkAlias = getSimpleNameAlias(sourceName);
151         }
152         if (checkAlias == null) {
153             checkAlias = getDefaultAlias(sourceName);
154         }
155         return checkAlias;
156     }
157 
158     /**
159      * Registers an alias for the source name of a check.
160      *
161      * @param sourceName the source name of the check (generally the class
162      *        name)
163      * @param checkAlias the alias used in {@link SuppressWarnings} annotations
164      */
165     private static void registerAlias(String sourceName, String checkAlias) {
166         CHECK_ALIAS_MAP.put(sourceName, checkAlias);
167     }
168 
169     /**
170      * Setter to specify aliases for check names that can be used in code
171      * within {@code SuppressWarnings} in a format of comma separated attribute=value entries.
172      * The attribute is the fully qualified name of the Check and value is its alias.
173      *
174      * @param aliasList comma-separated alias assignments
175      * @throws IllegalArgumentException when alias item does not have '='
176      * @since 5.7
177      */
178     public void setAliasList(String... aliasList) {
179         for (String sourceAlias : aliasList) {
180             final int index = sourceAlias.indexOf('=');
181             if (index > 0) {
182                 registerAlias(sourceAlias.substring(0, index), sourceAlias
183                     .substring(index + 1));
184             }
185             else if (!sourceAlias.isEmpty()) {
186                 throw new IllegalArgumentException(
187                     "'=' expected in alias list item: " + sourceAlias);
188             }
189         }
190     }
191 
192     /**
193      * Checks for a suppression of a check with the given source name and
194      * location in the last file processed.
195      *
196      * @param event audit event.
197      * @return whether the check with the given name is suppressed at the given
198      *         source location
199      */
200     public static boolean isSuppressed(AuditEvent event) {
201         final List<Entry> entries = ENTRIES.get();
202         final String sourceName = event.getSourceName();
203         final String checkAlias = getAlias(sourceName);
204         final int line = event.getLine();
205         final int column = event.getColumn();
206         boolean suppressed = false;
207         for (Entry entry : entries) {
208             final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
209             final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
210             final String checkName = entry.getCheckName();
211             final boolean nameMatches =
212                 ALL_WARNING_MATCHING_ID.equals(checkName)
213                     || checkName.equalsIgnoreCase(checkAlias)
214                     || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias)
215                     || getDefaultAlias(sourceName).equalsIgnoreCase(checkName);
216             if (afterStart && beforeEnd
217                     && (nameMatches || checkName.equals(event.getModuleId()))) {
218                 suppressed = true;
219                 break;
220             }
221         }
222         return suppressed;
223     }
224 
225     /**
226      * Checks whether suppression entry position is after the audit event occurrence position
227      * in the source file.
228      *
229      * @param line the line number in the source file where the event occurred.
230      * @param column the column number in the source file where the event occurred.
231      * @param entry suppression entry.
232      * @return true if suppression entry position is after the audit event occurrence position
233      *         in the source file.
234      */
235     private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
236         return entry.getFirstLine() < line
237             || entry.getFirstLine() == line
238             && (column == 0 || entry.getFirstColumn() <= column);
239     }
240 
241     /**
242      * Checks whether suppression entry position is before the audit event occurrence position
243      * in the source file.
244      *
245      * @param line the line number in the source file where the event occurred.
246      * @param column the column number in the source file where the event occurred.
247      * @param entry suppression entry.
248      * @return true if suppression entry position is before the audit event occurrence position
249      *         in the source file.
250      */
251     private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
252         return entry.getLastLine() > line
253             || entry.getLastLine() == line && entry
254                 .getLastColumn() >= column;
255     }
256 
257     @Override
258     public int[] getDefaultTokens() {
259         return getRequiredTokens();
260     }
261 
262     @Override
263     public int[] getAcceptableTokens() {
264         return getRequiredTokens();
265     }
266 
267     @Override
268     public int[] getRequiredTokens() {
269         return new int[] {TokenTypes.ANNOTATION};
270     }
271 
272     @Override
273     public void beginTree(DetailAST rootAST) {
274         ENTRIES.get().clear();
275     }
276 
277     @Override
278     public void visitToken(DetailAST ast) {
279         // check whether annotation is SuppressWarnings
280         // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
281         String identifier = getIdentifier(getNthChild(ast, 1));
282         if (identifier.startsWith(JAVA_LANG_PREFIX)) {
283             identifier = identifier.substring(JAVA_LANG_PREFIX.length());
284         }
285         if ("SuppressWarnings".equals(identifier)) {
286             getAnnotationTarget(ast).ifPresent(targetAST -> {
287                 addSuppressions(getAllAnnotationValues(ast), targetAST);
288             });
289         }
290     }
291 
292     /**
293      * Method to populate list of suppression entries.
294      *
295      * @param values
296      *            - list of check names
297      * @param targetAST
298      *            - annotation target
299      */
300     private static void addSuppressions(List<String> values, DetailAST targetAST) {
301         // get text range of target
302         final int firstLine = targetAST.getLineNo();
303         final int firstColumn = targetAST.getColumnNo();
304         final DetailAST nextAST = targetAST.getNextSibling();
305         final int lastLine;
306         final int lastColumn;
307         if (nextAST == null) {
308             lastLine = Integer.MAX_VALUE;
309             lastColumn = Integer.MAX_VALUE;
310         }
311         else {
312             lastLine = nextAST.getLineNo();
313             lastColumn = nextAST.getColumnNo();
314         }
315 
316         final List<Entry> entries = ENTRIES.get();
317         for (String value : values) {
318             // strip off the checkstyle-only prefix if present
319             final String checkName = removeCheckstylePrefixIfExists(value);
320             entries.add(new Entry(checkName, firstLine, firstColumn,
321                     lastLine, lastColumn));
322         }
323     }
324 
325     /**
326      * Method removes checkstyle prefix (checkstyle:) from check name if exists.
327      *
328      * @param checkName
329      *            - name of the check
330      * @return check name without prefix
331      */
332     private static String removeCheckstylePrefixIfExists(String checkName) {
333         String result = checkName;
334         if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
335             result = checkName.substring(CHECKSTYLE_PREFIX.length());
336         }
337         return result;
338     }
339 
340     /**
341      * Get all annotation values.
342      *
343      * @param ast annotation token
344      * @return list values
345      * @throws IllegalArgumentException if there is an unknown annotation value type.
346      */
347     private static List<String> getAllAnnotationValues(DetailAST ast) {
348         // get values of annotation
349         List<String> values = Collections.emptyList();
350         final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
351         if (lparenAST != null) {
352             final DetailAST nextAST = lparenAST.getNextSibling();
353             final int nextType = nextAST.getType();
354             switch (nextType) {
355                 case TokenTypes.EXPR:
356                 case TokenTypes.ANNOTATION_ARRAY_INIT:
357                     values = getAnnotationValues(nextAST);
358                     break;
359 
360                 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
361                     // expected children: IDENT ASSIGN ( EXPR |
362                     // ANNOTATION_ARRAY_INIT )
363                     values = getAnnotationValues(getNthChild(nextAST, 2));
364                     break;
365 
366                 case TokenTypes.RPAREN:
367                     // no value present (not valid Java)
368                     break;
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 -> Collections.singletonList(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 LinkedList<>();
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     /** Records a particular suppression for a region of a file. */
518     private static final class Entry {
519 
520         /** The source name of the suppressed check. */
521         private final String checkName;
522         /** The suppression region for the check - first line. */
523         private final int firstLine;
524         /** The suppression region for the check - first column. */
525         private final int firstColumn;
526         /** The suppression region for the check - last line. */
527         private final int lastLine;
528         /** The suppression region for the check - last column. */
529         private final int lastColumn;
530 
531         /**
532          * Constructs a new suppression region entry.
533          *
534          * @param checkName the source name of the suppressed check
535          * @param firstLine the first line of the suppression region
536          * @param firstColumn the first column of the suppression region
537          * @param lastLine the last line of the suppression region
538          * @param lastColumn the last column of the suppression region
539          */
540         private Entry(String checkName, int firstLine, int firstColumn,
541             int lastLine, int lastColumn) {
542             this.checkName = checkName;
543             this.firstLine = firstLine;
544             this.firstColumn = firstColumn;
545             this.lastLine = lastLine;
546             this.lastColumn = lastColumn;
547         }
548 
549         /**
550          * Gets the source name of the suppressed check.
551          *
552          * @return the source name of the suppressed check
553          */
554         public String getCheckName() {
555             return checkName;
556         }
557 
558         /**
559          * Gets the first line of the suppression region.
560          *
561          * @return the first line of the suppression region
562          */
563         public int getFirstLine() {
564             return firstLine;
565         }
566 
567         /**
568          * Gets the first column of the suppression region.
569          *
570          * @return the first column of the suppression region
571          */
572         public int getFirstColumn() {
573             return firstColumn;
574         }
575 
576         /**
577          * Gets the last line of the suppression region.
578          *
579          * @return the last line of the suppression region
580          */
581         public int getLastLine() {
582             return lastLine;
583         }
584 
585         /**
586          * Gets the last column of the suppression region.
587          *
588          * @return the last column of the suppression region
589          */
590         public int getLastColumn() {
591             return lastColumn;
592         }
593 
594     }
595 
596 }