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