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