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