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