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