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<>();
078
079    /**
080     * Returns the default alias for the source name of a check, which is the
081     * source name in lower case with any dotted prefix or "Check" suffix
082     * removed.
083     * @param sourceName the source name of the check (generally the class
084     *        name)
085     * @return the default alias for the given check
086     */
087    public static String getDefaultAlias(String sourceName) {
088        final int startIndex = sourceName.lastIndexOf('.') + 1;
089        int endIndex = sourceName.length();
090        if (sourceName.endsWith(CHECK_SUFFIX)) {
091            endIndex -= CHECK_SUFFIX.length();
092        }
093        return sourceName.substring(startIndex, endIndex).toLowerCase(Locale.ENGLISH);
094    }
095
096    /**
097     * Returns the alias for the source name of a check. If an alias has been
098     * explicitly registered via {@link #registerAlias(String, String)}, that
099     * 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    public 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.split(",")) {
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 ConversionException(
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 =
159                entry.getFirstLine() < line
160                    || entry.getFirstLine() == line
161                            && (column == 0 || entry.getFirstColumn() <= column);
162            final boolean beforeEnd =
163                entry.getLastLine() > line
164                    || entry.getLastLine() == line && entry
165                        .getLastColumn() >= column;
166            final boolean nameMatches =
167                ALL_WARNING_MATCHING_ID.equals(entry.getCheckName())
168                    || entry.getCheckName().equalsIgnoreCase(checkAlias);
169            final boolean idMatches = event.getModuleId() != null
170                && event.getModuleId().equals(entry.getCheckName());
171            if (afterStart && beforeEnd && (nameMatches || idMatches)) {
172                suppressed = true;
173            }
174        }
175        return suppressed;
176    }
177
178    @Override
179    public int[] getDefaultTokens() {
180        return getAcceptableTokens();
181    }
182
183    @Override
184    public int[] getAcceptableTokens() {
185        return new int[] {TokenTypes.ANNOTATION};
186    }
187
188    @Override
189    public int[] getRequiredTokens() {
190        return getAcceptableTokens();
191    }
192
193    @Override
194    public void beginTree(DetailAST rootAST) {
195        ENTRIES.set(new LinkedList<Entry>());
196    }
197
198    @Override
199    public void visitToken(DetailAST ast) {
200        // check whether annotation is SuppressWarnings
201        // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
202        String identifier = getIdentifier(getNthChild(ast, 1));
203        if (identifier.startsWith(JAVA_LANG_PREFIX)) {
204            identifier = identifier.substring(JAVA_LANG_PREFIX.length());
205        }
206        if ("SuppressWarnings".equals(identifier)) {
207
208            final List<String> values = getAllAnnotationValues(ast);
209            if (isAnnotationEmpty(values)) {
210                return;
211            }
212
213            final DetailAST targetAST = getAnnotationTarget(ast);
214
215            if (targetAST == null) {
216                log(ast.getLineNo(), MSG_KEY);
217                return;
218            }
219
220            // get text range of target
221            final int firstLine = targetAST.getLineNo();
222            final int firstColumn = targetAST.getColumnNo();
223            final DetailAST nextAST = targetAST.getNextSibling();
224            final int lastLine;
225            final int lastColumn;
226            if (nextAST == null) {
227                lastLine = Integer.MAX_VALUE;
228                lastColumn = Integer.MAX_VALUE;
229            }
230            else {
231                lastLine = nextAST.getLineNo();
232                lastColumn = nextAST.getColumnNo() - 1;
233            }
234
235            // add suppression entries for listed checks
236            final List<Entry> entries = ENTRIES.get();
237            for (String value : values) {
238                String checkName = value;
239                // strip off the checkstyle-only prefix if present
240                checkName = removeCheckstylePrefixIfExists(checkName);
241                entries.add(new Entry(checkName, firstLine, firstColumn,
242                        lastLine, lastColumn));
243            }
244        }
245    }
246
247    /**
248     * Method removes checkstyle prefix (checkstyle:) from check name if exists.
249     *
250     * @param checkName
251     *            - name of the check
252     * @return check name without prefix
253     */
254    private static String removeCheckstylePrefixIfExists(String checkName) {
255        String result = checkName;
256        if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
257            result = checkName.substring(CHECKSTYLE_PREFIX.length());
258        }
259        return result;
260    }
261
262    /**
263     * Get all annotation values.
264     * @param ast annotation token
265     * @return list values
266     */
267    private static List<String> getAllAnnotationValues(DetailAST ast) {
268        // get values of annotation
269        List<String> values = null;
270        final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
271        if (lparenAST != null) {
272            final DetailAST nextAST = lparenAST.getNextSibling();
273            final int nextType = nextAST.getType();
274            switch (nextType) {
275                case TokenTypes.EXPR:
276                case TokenTypes.ANNOTATION_ARRAY_INIT:
277                    values = getAnnotationValues(nextAST);
278                    break;
279
280                case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
281                    // expected children: IDENT ASSIGN ( EXPR |
282                    // ANNOTATION_ARRAY_INIT )
283                    values = getAnnotationValues(getNthChild(nextAST, 2));
284                    break;
285
286                case TokenTypes.RPAREN:
287                    // no value present (not valid Java)
288                    break;
289
290                default:
291                    // unknown annotation value type (new syntax?)
292                    throw new IllegalArgumentException("Unexpected AST: " + nextAST);
293            }
294        }
295        return values;
296    }
297
298    /**
299     * Checks that annotation is empty.
300     * @param values list of values in the annotation
301     * @return whether annotation is empty or contains some values
302     */
303    private static boolean isAnnotationEmpty(List<String> values) {
304        return values == null;
305    }
306
307    /**
308     * Get target of annotation.
309     * @param ast the AST node to get the child of
310     * @return get target of annotation
311     */
312    private static DetailAST getAnnotationTarget(DetailAST ast) {
313        final DetailAST targetAST;
314        final DetailAST parentAST = ast.getParent();
315        switch (parentAST.getType()) {
316            case TokenTypes.MODIFIERS:
317            case TokenTypes.ANNOTATIONS:
318                targetAST = getAcceptableParent(parentAST);
319                break;
320            default:
321                // unexpected container type
322                throw new IllegalArgumentException("Unexpected container AST: " + parentAST);
323        }
324        return targetAST;
325    }
326
327    /**
328     * Returns parent of given ast if parent has one of the following types:
329     * ANNOTATION_DEF, PACKAGE_DEF, CLASS_DEF, ENUM_DEF, ENUM_CONSTANT_DEF, CTOR_DEF,
330     * METHOD_DEF, PARAMETER_DEF, VARIABLE_DEF, ANNOTATION_FIELD_DEF, TYPE, LITERAL_NEW,
331     * LITERAL_THROWS, TYPE_ARGUMENT, IMPLEMENTS_CLAUSE, DOT.
332     * @param child an ast
333     * @return returns ast - parent of given
334     */
335    private static DetailAST getAcceptableParent(DetailAST child) {
336        final DetailAST result;
337        final DetailAST parent = child.getParent();
338        switch (parent.getType()) {
339            case TokenTypes.ANNOTATION_DEF:
340            case TokenTypes.PACKAGE_DEF:
341            case TokenTypes.CLASS_DEF:
342            case TokenTypes.INTERFACE_DEF:
343            case TokenTypes.ENUM_DEF:
344            case TokenTypes.ENUM_CONSTANT_DEF:
345            case TokenTypes.CTOR_DEF:
346            case TokenTypes.METHOD_DEF:
347            case TokenTypes.PARAMETER_DEF:
348            case TokenTypes.VARIABLE_DEF:
349            case TokenTypes.ANNOTATION_FIELD_DEF:
350            case TokenTypes.TYPE:
351            case TokenTypes.LITERAL_NEW:
352            case TokenTypes.LITERAL_THROWS:
353            case TokenTypes.TYPE_ARGUMENT:
354            case TokenTypes.IMPLEMENTS_CLAUSE:
355            case TokenTypes.DOT:
356                result = parent;
357                break;
358            default:
359                // it's possible case, but shouldn't be processed here
360                result = null;
361        }
362        return result;
363    }
364
365    /**
366     * Returns the n'th child of an AST node.
367     * @param ast the AST node to get the child of
368     * @param index the index of the child to get
369     * @return the n'th child of the given AST node, or {@code null} if none
370     */
371    private static DetailAST getNthChild(DetailAST ast, int index) {
372        DetailAST child = ast.getFirstChild();
373        for (int i = 0; i < index && child != null; ++i) {
374            child = child.getNextSibling();
375        }
376        return child;
377    }
378
379    /**
380     * Returns the Java identifier represented by an AST.
381     * @param ast an AST node for an IDENT or DOT
382     * @return the Java identifier represented by the given AST subtree
383     * @throws IllegalArgumentException if the AST is invalid
384     */
385    private static String getIdentifier(DetailAST ast) {
386        if (ast != null) {
387            if (ast.getType() == TokenTypes.IDENT) {
388                return ast.getText();
389            }
390            else {
391                return getIdentifier(ast.getFirstChild()) + "."
392                        + getIdentifier(ast.getLastChild());
393            }
394        }
395        throw new IllegalArgumentException("Identifier AST expected, but get null.");
396    }
397
398    /**
399     * Returns the literal string expression represented by an AST.
400     * @param ast an AST node for an EXPR
401     * @return the Java string represented by the given AST expression
402     *         or empty string if expression is too complex
403     * @throws IllegalArgumentException if the AST is invalid
404     */
405    private static String getStringExpr(DetailAST ast) {
406        final DetailAST firstChild = ast.getFirstChild();
407        String expr = "";
408
409        switch (firstChild.getType()) {
410            case TokenTypes.STRING_LITERAL:
411                // NOTE: escaped characters are not unescaped
412                final String quotedText = firstChild.getText();
413                expr = quotedText.substring(1, quotedText.length() - 1);
414                break;
415            case TokenTypes.IDENT:
416                expr = firstChild.getText();
417                break;
418            case TokenTypes.DOT:
419                expr = firstChild.getLastChild().getText();
420                break;
421            default:
422                // annotations with complex expressions cannot suppress warnings
423        }
424        return expr;
425    }
426
427    /**
428     * Returns the annotation values represented by an AST.
429     * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
430     * @return the list of Java string represented by the given AST for an
431     *         expression or annotation array initializer
432     * @throws IllegalArgumentException if the AST is invalid
433     */
434    private static List<String> getAnnotationValues(DetailAST ast) {
435        switch (ast.getType()) {
436            case TokenTypes.EXPR:
437                return ImmutableList.of(getStringExpr(ast));
438
439            case TokenTypes.ANNOTATION_ARRAY_INIT:
440                return findAllExpressionsInChildren(ast);
441
442            default:
443                throw new IllegalArgumentException(
444                        "Expression or annotation array initializer AST expected: " + ast);
445        }
446    }
447
448    /**
449     * Method looks at children and returns list of expressions in strings.
450     * @param parent ast, that contains children
451     * @return list of expressions in strings
452     */
453    private static List<String> findAllExpressionsInChildren(DetailAST parent) {
454        final List<String> valueList = Lists.newLinkedList();
455        DetailAST childAST = parent.getFirstChild();
456        while (childAST != null) {
457            if (childAST.getType() == TokenTypes.EXPR) {
458                valueList.add(getStringExpr(childAST));
459            }
460            childAST = childAST.getNextSibling();
461        }
462        return valueList;
463    }
464
465    /** Records a particular suppression for a region of a file. */
466    private static class Entry {
467        /** The source name of the suppressed check. */
468        private final String checkName;
469        /** The suppression region for the check - first line. */
470        private final int firstLine;
471        /** The suppression region for the check - first column. */
472        private final int firstColumn;
473        /** The suppression region for the check - last line. */
474        private final int lastLine;
475        /** The suppression region for the check - last column. */
476        private final int lastColumn;
477
478        /**
479         * Constructs a new suppression region entry.
480         * @param checkName the source name of the suppressed check
481         * @param firstLine the first line of the suppression region
482         * @param firstColumn the first column of the suppression region
483         * @param lastLine the last line of the suppression region
484         * @param lastColumn the last column of the suppression region
485         */
486        Entry(String checkName, int firstLine, int firstColumn,
487            int lastLine, int lastColumn) {
488            this.checkName = checkName;
489            this.firstLine = firstLine;
490            this.firstColumn = firstColumn;
491            this.lastLine = lastLine;
492            this.lastColumn = lastColumn;
493        }
494
495        /**
496         * Gets he source name of the suppressed check.
497         * @return the source name of the suppressed check
498         */
499        public String getCheckName() {
500            return checkName;
501        }
502
503        /**
504         * Gets the first line of the suppression region.
505         * @return the first line of the suppression region
506         */
507        public int getFirstLine() {
508            return firstLine;
509        }
510
511        /**
512         * Gets the first column of the suppression region.
513         * @return the first column of the suppression region
514         */
515        public int getFirstColumn() {
516            return firstColumn;
517        }
518
519        /**
520         * Gets the last line of the suppression region.
521         * @return the last line of the suppression region
522         */
523        public int getLastLine() {
524            return lastLine;
525        }
526
527        /**
528         * Gets the last column of the suppression region.
529         * @return the last column of the suppression region
530         */
531        public int getLastColumn() {
532            return lastColumn;
533        }
534    }
535}