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