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