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