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