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