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