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