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