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