001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks;
021
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Optional;
029import java.util.regex.Pattern;
030
031import com.puppycrawl.tools.checkstyle.StatelessCheck;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.AuditEvent;
034import com.puppycrawl.tools.checkstyle.api.DetailAST;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036
037/**
038 * <p>
039 * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations.
040 * It allows to prevent Checkstyle from reporting violations from parts of code that were
041 * annotated with {@code @SuppressWarnings} and using name of the check to be excluded.
042 * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}.
043 * You can also use a {@code checkstyle:} prefix to prevent compiler
044 * from processing these annotations.
045 * You can also define aliases for check names that need to be suppressed.
046 * </p>
047 * <ul>
048 * <li>
049 * Property {@code aliasList} - Specify aliases for check names that can be used in code
050 * within {@code SuppressWarnings}.
051 * Type is {@code java.lang.String[]}.
052 * Default value is {@code null}.
053 * </li>
054 * </ul>
055 * <p>
056 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
057 * </p>
058 *
059 * @since 5.7
060 */
061@StatelessCheck
062public class SuppressWarningsHolder
063    extends AbstractCheck {
064
065    /**
066     * Optional prefix for warning suppressions that are only intended to be
067     * recognized by checkstyle. For instance, to suppress {@code
068     * FallThroughCheck} only in checkstyle (and not in javac), use the
069     * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
070     * To suppress the warning in both tools, just use {@code "fallthrough"}.
071     */
072    private static final String CHECKSTYLE_PREFIX = "checkstyle:";
073
074    /** Java.lang namespace prefix, which is stripped from SuppressWarnings */
075    private static final String JAVA_LANG_PREFIX = "java.lang.";
076
077    /** Suffix to be removed from subclasses of Check. */
078    private static final String CHECK_SUFFIX = "check";
079
080    /** Special warning id for matching all the warnings. */
081    private static final String ALL_WARNING_MATCHING_ID = "all";
082
083    /** A map from check source names to suppression aliases. */
084    private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
085
086    /**
087     * A thread-local holder for the list of suppression entries for the last
088     * file parsed.
089     */
090    private static final ThreadLocal<List<Entry>> ENTRIES =
091            ThreadLocal.withInitial(LinkedList::new);
092
093    /**
094     * Compiled pattern used to match whitespace in text block content.
095     */
096    private static final Pattern WHITESPACE = Pattern.compile("\\s+");
097
098    /**
099     * Compiled pattern used to match preceding newline in text block content.
100     */
101    private static final Pattern NEWLINE = Pattern.compile("\\n");
102
103    /**
104     * Returns the default alias for the source name of a check, which is the
105     * source name in lower case with any dotted prefix or "Check"/"check"
106     * suffix removed.
107     *
108     * @param sourceName the source name of the check (generally the class
109     *        name)
110     * @return the default alias for the given check
111     */
112    public static String getDefaultAlias(String sourceName) {
113        int endIndex = sourceName.length();
114        final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH);
115        if (sourceNameLower.endsWith(CHECK_SUFFIX)) {
116            endIndex -= CHECK_SUFFIX.length();
117        }
118        final int startIndex = sourceNameLower.lastIndexOf('.') + 1;
119        return sourceNameLower.substring(startIndex, endIndex);
120    }
121
122    /**
123     * Returns the alias for the source name of a check. If an alias has been
124     * explicitly registered via {@link #setAliasList(String...)}, that
125     * alias is returned; otherwise, the default alias is used.
126     *
127     * @param sourceName the source name of the check (generally the class
128     *        name)
129     * @return the current alias for the given check
130     */
131    public static String getAlias(String sourceName) {
132        String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
133        if (checkAlias == null) {
134            checkAlias = getDefaultAlias(sourceName);
135        }
136        return checkAlias;
137    }
138
139    /**
140     * Registers an alias for the source name of a check.
141     *
142     * @param sourceName the source name of the check (generally the class
143     *        name)
144     * @param checkAlias the alias used in {@link SuppressWarnings} annotations
145     */
146    private static void registerAlias(String sourceName, String checkAlias) {
147        CHECK_ALIAS_MAP.put(sourceName, checkAlias);
148    }
149
150    /**
151     * Setter to specify aliases for check names that can be used in code
152     * within {@code SuppressWarnings}.
153     *
154     * @param aliasList comma-separated alias assignments
155     * @throws IllegalArgumentException when alias item does not have '='
156     */
157    public void setAliasList(String... aliasList) {
158        for (String sourceAlias : aliasList) {
159            final int index = sourceAlias.indexOf('=');
160            if (index > 0) {
161                registerAlias(sourceAlias.substring(0, index), sourceAlias
162                    .substring(index + 1));
163            }
164            else if (!sourceAlias.isEmpty()) {
165                throw new IllegalArgumentException(
166                    "'=' expected in alias list item: " + sourceAlias);
167            }
168        }
169    }
170
171    /**
172     * Checks for a suppression of a check with the given source name and
173     * location in the last file processed.
174     *
175     * @param event audit event.
176     * @return whether the check with the given name is suppressed at the given
177     *         source location
178     */
179    public static boolean isSuppressed(AuditEvent event) {
180        final List<Entry> entries = ENTRIES.get();
181        final String sourceName = event.getSourceName();
182        final String checkAlias = getAlias(sourceName);
183        final int line = event.getLine();
184        final int column = event.getColumn();
185        boolean suppressed = false;
186        for (Entry entry : entries) {
187            final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
188            final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
189            final String checkName = entry.getCheckName();
190            final boolean nameMatches =
191                ALL_WARNING_MATCHING_ID.equals(checkName)
192                    || checkName.equalsIgnoreCase(checkAlias)
193                    || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias);
194            if (afterStart && beforeEnd
195                    && (nameMatches || checkName.equals(event.getModuleId()))) {
196                suppressed = true;
197                break;
198            }
199        }
200        return suppressed;
201    }
202
203    /**
204     * Checks whether suppression entry position is after the audit event occurrence position
205     * in the source file.
206     *
207     * @param line the line number in the source file where the event occurred.
208     * @param column the column number in the source file where the event occurred.
209     * @param entry suppression entry.
210     * @return true if suppression entry position is after the audit event occurrence position
211     *         in the source file.
212     */
213    private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
214        return entry.getFirstLine() < line
215            || entry.getFirstLine() == line
216            && (column == 0 || entry.getFirstColumn() <= column);
217    }
218
219    /**
220     * Checks whether suppression entry position is before the audit event occurrence position
221     * in the source file.
222     *
223     * @param line the line number in the source file where the event occurred.
224     * @param column the column number in the source file where the event occurred.
225     * @param entry suppression entry.
226     * @return true if suppression entry position is before the audit event occurrence position
227     *         in the source file.
228     */
229    private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
230        return entry.getLastLine() > line
231            || entry.getLastLine() == line && entry
232                .getLastColumn() >= column;
233    }
234
235    @Override
236    public int[] getDefaultTokens() {
237        return getRequiredTokens();
238    }
239
240    @Override
241    public int[] getAcceptableTokens() {
242        return getRequiredTokens();
243    }
244
245    @Override
246    public int[] getRequiredTokens() {
247        return new int[] {TokenTypes.ANNOTATION};
248    }
249
250    @Override
251    public void beginTree(DetailAST rootAST) {
252        ENTRIES.get().clear();
253    }
254
255    @Override
256    public void visitToken(DetailAST ast) {
257        // check whether annotation is SuppressWarnings
258        // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
259        String identifier = getIdentifier(getNthChild(ast, 1));
260        if (identifier.startsWith(JAVA_LANG_PREFIX)) {
261            identifier = identifier.substring(JAVA_LANG_PREFIX.length());
262        }
263        if ("SuppressWarnings".equals(identifier)) {
264            getAnnotationTarget(ast).ifPresent(targetAST -> {
265                addSuppressions(getAllAnnotationValues(ast), targetAST);
266            });
267        }
268    }
269
270    /**
271     * Method to populate list of suppression entries.
272     *
273     * @param values
274     *            - list of check names
275     * @param targetAST
276     *            - annotation target
277     */
278    private static void addSuppressions(List<String> values, DetailAST targetAST) {
279        // get text range of target
280        final int firstLine = targetAST.getLineNo();
281        final int firstColumn = targetAST.getColumnNo();
282        final DetailAST nextAST = targetAST.getNextSibling();
283        final int lastLine;
284        final int lastColumn;
285        if (nextAST == null) {
286            lastLine = Integer.MAX_VALUE;
287            lastColumn = Integer.MAX_VALUE;
288        }
289        else {
290            lastLine = nextAST.getLineNo();
291            lastColumn = nextAST.getColumnNo();
292        }
293
294        final List<Entry> entries = ENTRIES.get();
295        for (String value : values) {
296            // strip off the checkstyle-only prefix if present
297            final String checkName = removeCheckstylePrefixIfExists(value);
298            entries.add(new Entry(checkName, firstLine, firstColumn,
299                    lastLine, lastColumn));
300        }
301    }
302
303    /**
304     * Method removes checkstyle prefix (checkstyle:) from check name if exists.
305     *
306     * @param checkName
307     *            - name of the check
308     * @return check name without prefix
309     */
310    private static String removeCheckstylePrefixIfExists(String checkName) {
311        String result = checkName;
312        if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
313            result = checkName.substring(CHECKSTYLE_PREFIX.length());
314        }
315        return result;
316    }
317
318    /**
319     * Get all annotation values.
320     *
321     * @param ast annotation token
322     * @return list values
323     * @throws IllegalArgumentException if there is an unknown annotation value type.
324     */
325    private static List<String> getAllAnnotationValues(DetailAST ast) {
326        // get values of annotation
327        List<String> values = Collections.emptyList();
328        final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
329        if (lparenAST != null) {
330            final DetailAST nextAST = lparenAST.getNextSibling();
331            final int nextType = nextAST.getType();
332            switch (nextType) {
333                case TokenTypes.EXPR:
334                case TokenTypes.ANNOTATION_ARRAY_INIT:
335                    values = getAnnotationValues(nextAST);
336                    break;
337
338                case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
339                    // expected children: IDENT ASSIGN ( EXPR |
340                    // ANNOTATION_ARRAY_INIT )
341                    values = getAnnotationValues(getNthChild(nextAST, 2));
342                    break;
343
344                case TokenTypes.RPAREN:
345                    // no value present (not valid Java)
346                    break;
347
348                default:
349                    // unknown annotation value type (new syntax?)
350                    throw new IllegalArgumentException("Unexpected AST: " + nextAST);
351            }
352        }
353        return values;
354    }
355
356    /**
357     * Get target of annotation.
358     *
359     * @param ast the AST node to get the child of
360     * @return get target of annotation
361     * @throws IllegalArgumentException if there is an unexpected container type.
362     */
363    private static Optional<DetailAST> getAnnotationTarget(DetailAST ast) {
364        final Optional<DetailAST> result;
365        final DetailAST parentAST = ast.getParent();
366        switch (parentAST.getType()) {
367            case TokenTypes.MODIFIERS:
368            case TokenTypes.ANNOTATIONS:
369            case TokenTypes.ANNOTATION:
370            case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
371                result = Optional.of(parentAST.getParent());
372                break;
373            case TokenTypes.LITERAL_DEFAULT:
374                result = Optional.empty();
375                break;
376            case TokenTypes.ANNOTATION_ARRAY_INIT:
377                result = getAnnotationTarget(parentAST);
378                break;
379            default:
380                // unexpected container type
381                throw new IllegalArgumentException("Unexpected container AST: " + parentAST);
382        }
383        return result;
384    }
385
386    /**
387     * Returns the n'th child of an AST node.
388     *
389     * @param ast the AST node to get the child of
390     * @param index the index of the child to get
391     * @return the n'th child of the given AST node, or {@code null} if none
392     */
393    private static DetailAST getNthChild(DetailAST ast, int index) {
394        DetailAST child = ast.getFirstChild();
395        for (int i = 0; i < index && child != null; ++i) {
396            child = child.getNextSibling();
397        }
398        return child;
399    }
400
401    /**
402     * Returns the Java identifier represented by an AST.
403     *
404     * @param ast an AST node for an IDENT or DOT
405     * @return the Java identifier represented by the given AST subtree
406     * @throws IllegalArgumentException if the AST is invalid
407     */
408    private static String getIdentifier(DetailAST ast) {
409        if (ast == null) {
410            throw new IllegalArgumentException("Identifier AST expected, but get null.");
411        }
412        final String identifier;
413        if (ast.getType() == TokenTypes.IDENT) {
414            identifier = ast.getText();
415        }
416        else {
417            identifier = getIdentifier(ast.getFirstChild()) + "."
418                + getIdentifier(ast.getLastChild());
419        }
420        return identifier;
421    }
422
423    /**
424     * Returns the literal string expression represented by an AST.
425     *
426     * @param ast an AST node for an EXPR
427     * @return the Java string represented by the given AST expression
428     *         or empty string if expression is too complex
429     * @throws IllegalArgumentException if the AST is invalid
430     */
431    private static String getStringExpr(DetailAST ast) {
432        final DetailAST firstChild = ast.getFirstChild();
433        String expr = "";
434
435        switch (firstChild.getType()) {
436            case TokenTypes.STRING_LITERAL:
437                // NOTE: escaped characters are not unescaped
438                final String quotedText = firstChild.getText();
439                expr = quotedText.substring(1, quotedText.length() - 1);
440                break;
441            case TokenTypes.IDENT:
442                expr = firstChild.getText();
443                break;
444            case TokenTypes.DOT:
445                expr = firstChild.getLastChild().getText();
446                break;
447            case TokenTypes.TEXT_BLOCK_LITERAL_BEGIN:
448                final String textBlockContent = firstChild.getFirstChild().getText();
449                expr = getContentWithoutPrecedingWhitespace(textBlockContent);
450                break;
451            default:
452                // annotations with complex expressions cannot suppress warnings
453        }
454        return expr;
455    }
456
457    /**
458     * Returns the annotation values represented by an AST.
459     *
460     * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
461     * @return the list of Java string represented by the given AST for an
462     *         expression or annotation array initializer
463     * @throws IllegalArgumentException if the AST is invalid
464     */
465    private static List<String> getAnnotationValues(DetailAST ast) {
466        final List<String> annotationValues;
467        switch (ast.getType()) {
468            case TokenTypes.EXPR:
469                annotationValues = Collections.singletonList(getStringExpr(ast));
470                break;
471            case TokenTypes.ANNOTATION_ARRAY_INIT:
472                annotationValues = findAllExpressionsInChildren(ast);
473                break;
474            default:
475                throw new IllegalArgumentException(
476                        "Expression or annotation array initializer AST expected: " + ast);
477        }
478        return annotationValues;
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}