001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks;
021
022import java.util.HashMap;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.commons.beanutils.ConversionException;
029
030import com.google.common.collect.ImmutableList;
031import com.google.common.collect.Lists;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.AuditEvent;
034import com.puppycrawl.tools.checkstyle.api.DetailAST;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036
037/**
038 * Maintains a set of check suppressions from {@link SuppressWarnings}
039 * annotations.
040 * @author Trevor Robinson
041 * @author Stéphane Galland
042 */
043public class SuppressWarningsHolder
044    extends AbstractCheck {
045
046    /**
047     * A key is pointing to the warning message text in "messages.properties"
048     * file.
049     */
050    public static final String MSG_KEY = "suppress.warnings.invalid.target";
051
052    /**
053     * Optional prefix for warning suppressions that are only intended to be
054     * recognized by checkstyle. For instance, to suppress {@code
055     * FallThroughCheck} only in checkstyle (and not in javac), use the
056     * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
057     * To suppress the warning in both tools, just use {@code "fallthrough"}.
058     */
059    public static final String CHECKSTYLE_PREFIX = "checkstyle:";
060
061    /** Java.lang namespace prefix, which is stripped from SuppressWarnings */
062    private static final String JAVA_LANG_PREFIX = "java.lang.";
063
064    /** Suffix to be removed from subclasses of Check. */
065    private static final String CHECK_SUFFIX = "Check";
066
067    /** Special warning id for matching all the warnings. */
068    private static final String ALL_WARNING_MATCHING_ID = "all";
069
070    /** A map from check source names to suppression aliases. */
071    private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
072
073    /**
074     * A thread-local holder for the list of suppression entries for the last
075     * file parsed.
076     */
077    private static final ThreadLocal<List<Entry>> ENTRIES = new ThreadLocal<List<Entry>>() {
078        @Override
079        protected List<Entry> initialValue() {
080            return new LinkedList<>();
081        }
082    };
083
084    /**
085     * Returns the default alias for the source name of a check, which is the
086     * source name in lower case with any dotted prefix or "Check" suffix
087     * removed.
088     * @param sourceName the source name of the check (generally the class
089     *        name)
090     * @return the default alias for the given check
091     */
092    public static String getDefaultAlias(String sourceName) {
093        final int startIndex = sourceName.lastIndexOf('.') + 1;
094        int endIndex = sourceName.length();
095        if (sourceName.endsWith(CHECK_SUFFIX)) {
096            endIndex -= CHECK_SUFFIX.length();
097        }
098        return sourceName.substring(startIndex, endIndex).toLowerCase(Locale.ENGLISH);
099    }
100
101    /**
102     * Returns the alias for the source name of a check. If an alias has been
103     * explicitly registered via {@link #registerAlias(String, String)}, that
104     * alias is returned; otherwise, the default alias is used.
105     * @param sourceName the source name of the check (generally the class
106     *        name)
107     * @return the current alias for the given check
108     */
109    public static String getAlias(String sourceName) {
110        String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
111        if (checkAlias == null) {
112            checkAlias = getDefaultAlias(sourceName);
113        }
114        return checkAlias;
115    }
116
117    /**
118     * Registers an alias for the source name of a check.
119     * @param sourceName the source name of the check (generally the class
120     *        name)
121     * @param checkAlias the alias used in {@link SuppressWarnings} annotations
122     */
123    public static void registerAlias(String sourceName, String checkAlias) {
124        CHECK_ALIAS_MAP.put(sourceName, checkAlias);
125    }
126
127    /**
128     * Registers a list of source name aliases based on a comma-separated list
129     * of {@code source=alias} items, such as {@code
130     * com.puppycrawl.tools.checkstyle.checks.sizes.ParameterNumberCheck=
131     * paramnum}.
132     * @param aliasList the list of comma-separated alias assignments
133     */
134    public void setAliasList(String... aliasList) {
135        for (String sourceAlias : aliasList) {
136            final int index = sourceAlias.indexOf('=');
137            if (index > 0) {
138                registerAlias(sourceAlias.substring(0, index), sourceAlias
139                    .substring(index + 1));
140            }
141            else if (!sourceAlias.isEmpty()) {
142                throw new ConversionException(
143                    "'=' expected in alias list item: " + sourceAlias);
144            }
145        }
146    }
147
148    /**
149     * Checks for a suppression of a check with the given source name and
150     * location in the last file processed.
151     * @param event audit event.
152     * @return whether the check with the given name is suppressed at the given
153     *         source location
154     */
155    public static boolean isSuppressed(AuditEvent event) {
156        final List<Entry> entries = ENTRIES.get();
157        final String sourceName = event.getSourceName();
158        final String checkAlias = getAlias(sourceName);
159        final int line = event.getLine();
160        final int column = event.getColumn();
161        boolean suppressed = false;
162        for (Entry entry : entries) {
163            final boolean afterStart =
164                entry.getFirstLine() < line
165                    || entry.getFirstLine() == line
166                            && (column == 0 || entry.getFirstColumn() <= column);
167            final boolean beforeEnd =
168                entry.getLastLine() > line
169                    || entry.getLastLine() == line && entry
170                        .getLastColumn() >= column;
171            final boolean nameMatches =
172                ALL_WARNING_MATCHING_ID.equals(entry.getCheckName())
173                    || entry.getCheckName().equalsIgnoreCase(checkAlias);
174            final boolean idMatches = event.getModuleId() != null
175                && event.getModuleId().equals(entry.getCheckName());
176            if (afterStart && beforeEnd && (nameMatches || idMatches)) {
177                suppressed = true;
178            }
179        }
180        return suppressed;
181    }
182
183    @Override
184    public int[] getDefaultTokens() {
185        return getAcceptableTokens();
186    }
187
188    @Override
189    public int[] getAcceptableTokens() {
190        return new int[] {TokenTypes.ANNOTATION};
191    }
192
193    @Override
194    public int[] getRequiredTokens() {
195        return getAcceptableTokens();
196    }
197
198    @Override
199    public void beginTree(DetailAST rootAST) {
200        ENTRIES.get().clear();
201    }
202
203    @Override
204    public void visitToken(DetailAST ast) {
205        // check whether annotation is SuppressWarnings
206        // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
207        String identifier = getIdentifier(getNthChild(ast, 1));
208        if (identifier.startsWith(JAVA_LANG_PREFIX)) {
209            identifier = identifier.substring(JAVA_LANG_PREFIX.length());
210        }
211        if ("SuppressWarnings".equals(identifier)) {
212
213            final List<String> values = getAllAnnotationValues(ast);
214            if (!isAnnotationEmpty(values)) {
215                final DetailAST targetAST = getAnnotationTarget(ast);
216
217                if (targetAST == null) {
218                    log(ast.getLineNo(), MSG_KEY);
219                }
220                else {
221                    // get text range of target
222                    final int firstLine = targetAST.getLineNo();
223                    final int firstColumn = targetAST.getColumnNo();
224                    final DetailAST nextAST = targetAST.getNextSibling();
225                    final int lastLine;
226                    final int lastColumn;
227                    if (nextAST == null) {
228                        lastLine = Integer.MAX_VALUE;
229                        lastColumn = Integer.MAX_VALUE;
230                    }
231                    else {
232                        lastLine = nextAST.getLineNo();
233                        lastColumn = nextAST.getColumnNo() - 1;
234                    }
235
236                    // add suppression entries for listed checks
237                    final List<Entry> entries = ENTRIES.get();
238                    for (String value : values) {
239                        String checkName = value;
240                        // strip off the checkstyle-only prefix if present
241                        checkName = removeCheckstylePrefixIfExists(checkName);
242                        entries.add(new Entry(checkName, firstLine, firstColumn,
243                                lastLine, lastColumn));
244                    }
245                }
246            }
247        }
248    }
249
250    /**
251     * Method removes checkstyle prefix (checkstyle:) from check name if exists.
252     *
253     * @param checkName
254     *            - name of the check
255     * @return check name without prefix
256     */
257    private static String removeCheckstylePrefixIfExists(String checkName) {
258        String result = checkName;
259        if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
260            result = checkName.substring(CHECKSTYLE_PREFIX.length());
261        }
262        return result;
263    }
264
265    /**
266     * Get all annotation values.
267     * @param ast annotation token
268     * @return list values
269     */
270    private static List<String> getAllAnnotationValues(DetailAST ast) {
271        // get values of annotation
272        List<String> values = null;
273        final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
274        if (lparenAST != null) {
275            final DetailAST nextAST = lparenAST.getNextSibling();
276            final int nextType = nextAST.getType();
277            switch (nextType) {
278                case TokenTypes.EXPR:
279                case TokenTypes.ANNOTATION_ARRAY_INIT:
280                    values = getAnnotationValues(nextAST);
281                    break;
282
283                case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
284                    // expected children: IDENT ASSIGN ( EXPR |
285                    // ANNOTATION_ARRAY_INIT )
286                    values = getAnnotationValues(getNthChild(nextAST, 2));
287                    break;
288
289                case TokenTypes.RPAREN:
290                    // no value present (not valid Java)
291                    break;
292
293                default:
294                    // unknown annotation value type (new syntax?)
295                    throw new IllegalArgumentException("Unexpected AST: " + nextAST);
296            }
297        }
298        return values;
299    }
300
301    /**
302     * Checks that annotation is empty.
303     * @param values list of values in the annotation
304     * @return whether annotation is empty or contains some values
305     */
306    private static boolean isAnnotationEmpty(List<String> values) {
307        return values == null;
308    }
309
310    /**
311     * Get target of annotation.
312     * @param ast the AST node to get the child of
313     * @return get target of annotation
314     */
315    private static DetailAST getAnnotationTarget(DetailAST ast) {
316        final DetailAST targetAST;
317        final DetailAST parentAST = ast.getParent();
318        switch (parentAST.getType()) {
319            case TokenTypes.MODIFIERS:
320            case TokenTypes.ANNOTATIONS:
321                targetAST = getAcceptableParent(parentAST);
322                break;
323            default:
324                // unexpected container type
325                throw new IllegalArgumentException("Unexpected container AST: " + parentAST);
326        }
327        return targetAST;
328    }
329
330    /**
331     * Returns parent of given ast if parent has one of the following types:
332     * ANNOTATION_DEF, PACKAGE_DEF, CLASS_DEF, ENUM_DEF, ENUM_CONSTANT_DEF, CTOR_DEF,
333     * METHOD_DEF, PARAMETER_DEF, VARIABLE_DEF, ANNOTATION_FIELD_DEF, TYPE, LITERAL_NEW,
334     * LITERAL_THROWS, TYPE_ARGUMENT, IMPLEMENTS_CLAUSE, DOT.
335     * @param child an ast
336     * @return returns ast - parent of given
337     */
338    private static DetailAST getAcceptableParent(DetailAST child) {
339        final DetailAST result;
340        final DetailAST parent = child.getParent();
341        switch (parent.getType()) {
342            case TokenTypes.ANNOTATION_DEF:
343            case TokenTypes.PACKAGE_DEF:
344            case TokenTypes.CLASS_DEF:
345            case TokenTypes.INTERFACE_DEF:
346            case TokenTypes.ENUM_DEF:
347            case TokenTypes.ENUM_CONSTANT_DEF:
348            case TokenTypes.CTOR_DEF:
349            case TokenTypes.METHOD_DEF:
350            case TokenTypes.PARAMETER_DEF:
351            case TokenTypes.VARIABLE_DEF:
352            case TokenTypes.ANNOTATION_FIELD_DEF:
353            case TokenTypes.TYPE:
354            case TokenTypes.LITERAL_NEW:
355            case TokenTypes.LITERAL_THROWS:
356            case TokenTypes.TYPE_ARGUMENT:
357            case TokenTypes.IMPLEMENTS_CLAUSE:
358            case TokenTypes.DOT:
359                result = parent;
360                break;
361            default:
362                // it's possible case, but shouldn't be processed here
363                result = null;
364        }
365        return result;
366    }
367
368    /**
369     * Returns the n'th child of an AST node.
370     * @param ast the AST node to get the child of
371     * @param index the index of the child to get
372     * @return the n'th child of the given AST node, or {@code null} if none
373     */
374    private static DetailAST getNthChild(DetailAST ast, int index) {
375        DetailAST child = ast.getFirstChild();
376        for (int i = 0; i < index && child != null; ++i) {
377            child = child.getNextSibling();
378        }
379        return child;
380    }
381
382    /**
383     * Returns the Java identifier represented by an AST.
384     * @param ast an AST node for an IDENT or DOT
385     * @return the Java identifier represented by the given AST subtree
386     * @throws IllegalArgumentException if the AST is invalid
387     */
388    private static String getIdentifier(DetailAST ast) {
389        if (ast != null) {
390            if (ast.getType() == TokenTypes.IDENT) {
391                return ast.getText();
392            }
393            else {
394                return getIdentifier(ast.getFirstChild()) + "."
395                        + getIdentifier(ast.getLastChild());
396            }
397        }
398        throw new IllegalArgumentException("Identifier AST expected, but get null.");
399    }
400
401    /**
402     * Returns the literal string expression represented by an AST.
403     * @param ast an AST node for an EXPR
404     * @return the Java string represented by the given AST expression
405     *         or empty string if expression is too complex
406     * @throws IllegalArgumentException if the AST is invalid
407     */
408    private static String getStringExpr(DetailAST ast) {
409        final DetailAST firstChild = ast.getFirstChild();
410        String expr = "";
411
412        switch (firstChild.getType()) {
413            case TokenTypes.STRING_LITERAL:
414                // NOTE: escaped characters are not unescaped
415                final String quotedText = firstChild.getText();
416                expr = quotedText.substring(1, quotedText.length() - 1);
417                break;
418            case TokenTypes.IDENT:
419                expr = firstChild.getText();
420                break;
421            case TokenTypes.DOT:
422                expr = firstChild.getLastChild().getText();
423                break;
424            default:
425                // annotations with complex expressions cannot suppress warnings
426        }
427        return expr;
428    }
429
430    /**
431     * Returns the annotation values represented by an AST.
432     * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
433     * @return the list of Java string represented by the given AST for an
434     *         expression or annotation array initializer
435     * @throws IllegalArgumentException if the AST is invalid
436     */
437    private static List<String> getAnnotationValues(DetailAST ast) {
438        switch (ast.getType()) {
439            case TokenTypes.EXPR:
440                return ImmutableList.of(getStringExpr(ast));
441
442            case TokenTypes.ANNOTATION_ARRAY_INIT:
443                return findAllExpressionsInChildren(ast);
444
445            default:
446                throw new IllegalArgumentException(
447                        "Expression or annotation array initializer AST expected: " + ast);
448        }
449    }
450
451    /**
452     * Method looks at children and returns list of expressions in strings.
453     * @param parent ast, that contains children
454     * @return list of expressions in strings
455     */
456    private static List<String> findAllExpressionsInChildren(DetailAST parent) {
457        final List<String> valueList = Lists.newLinkedList();
458        DetailAST childAST = parent.getFirstChild();
459        while (childAST != null) {
460            if (childAST.getType() == TokenTypes.EXPR) {
461                valueList.add(getStringExpr(childAST));
462            }
463            childAST = childAST.getNextSibling();
464        }
465        return valueList;
466    }
467
468    /** Records a particular suppression for a region of a file. */
469    private static class Entry {
470        /** The source name of the suppressed check. */
471        private final String checkName;
472        /** The suppression region for the check - first line. */
473        private final int firstLine;
474        /** The suppression region for the check - first column. */
475        private final int firstColumn;
476        /** The suppression region for the check - last line. */
477        private final int lastLine;
478        /** The suppression region for the check - last column. */
479        private final int lastColumn;
480
481        /**
482         * Constructs a new suppression region entry.
483         * @param checkName the source name of the suppressed check
484         * @param firstLine the first line of the suppression region
485         * @param firstColumn the first column of the suppression region
486         * @param lastLine the last line of the suppression region
487         * @param lastColumn the last column of the suppression region
488         */
489        Entry(String checkName, int firstLine, int firstColumn,
490            int lastLine, int lastColumn) {
491            this.checkName = checkName;
492            this.firstLine = firstLine;
493            this.firstColumn = firstColumn;
494            this.lastLine = lastLine;
495            this.lastColumn = lastColumn;
496        }
497
498        /**
499         * Gets he source name of the suppressed check.
500         * @return the source name of the suppressed check
501         */
502        public String getCheckName() {
503            return checkName;
504        }
505
506        /**
507         * Gets the first line of the suppression region.
508         * @return the first line of the suppression region
509         */
510        public int getFirstLine() {
511            return firstLine;
512        }
513
514        /**
515         * Gets the first column of the suppression region.
516         * @return the first column of the suppression region
517         */
518        public int getFirstColumn() {
519            return firstColumn;
520        }
521
522        /**
523         * Gets the last line of the suppression region.
524         * @return the last line of the suppression region
525         */
526        public int getLastLine() {
527            return lastLine;
528        }
529
530        /**
531         * Gets the last column of the suppression region.
532         * @return the last column of the suppression region
533         */
534        public int getLastColumn() {
535            return lastColumn;
536        }
537    }
538}