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