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