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