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