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