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