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