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