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