001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.checks.annotation;
020
021import java.util.regex.Matcher;
022
023import com.puppycrawl.tools.checkstyle.api.AnnotationUtility;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026import com.puppycrawl.tools.checkstyle.checks.AbstractFormatCheck;
027
028/**
029 * <p>
030 * This check allows you to specify what warnings that
031 * {@link SuppressWarnings SuppressWarnings} is not
032 * allowed to suppress.  You can also specify a list
033 * of TokenTypes that the configured warning(s) cannot
034 * be suppressed on.
035 * </p>
036 *
037 * <p>
038 * The {@link AbstractFormatCheck#setFormat warnings} property is a
039 * regex pattern.  Any warning being suppressed matching
040 * this pattern will be flagged.
041 * </p>
042 *
043 * <p>
044 * By default, any warning specified will be disallowed on
045 * all legal TokenTypes unless otherwise specified via
046 * the
047 * {@link com.puppycrawl.tools.checkstyle.api.Check#setTokens(String[]) tokens}
048 * property.
049 *
050 * Also, by default warnings that are empty strings or all
051 * whitespace (regex: ^$|^\s+$) are flagged.  By specifying,
052 * the format property these defaults no longer apply.
053 * </p>
054 *
055 * <p>
056 * Limitations:  This check does not consider conditionals
057 * inside the SuppressWarnings annotation. <br>
058 * For example:
059 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}
060 * According to the above example, the "unused" warning is being suppressed
061 * not the "unchecked" or "foo" warnings.  All of these warnings will be
062 * considered and matched against regardless of what the conditional
063 * evaluates to.
064 * </p>
065 *
066 * <p>
067 * This check can be configured so that the "unchecked"
068 * and "unused" warnings cannot be suppressed on
069 * anything but variable and parameter declarations.
070 * See below of an example.
071 * </p>
072 *
073 * <pre>
074 * &lt;module name=&quot;SuppressWarnings&quot;&gt;
075 *    &lt;property name=&quot;format&quot;
076 *        value=&quot;^unchecked$|^unused$&quot;/&gt;
077 *    &lt;property name=&quot;tokens&quot;
078 *        value=&quot;
079 *        CLASS_DEF,INTERFACE_DEF,ENUM_DEF,
080 *        ANNOTATION_DEF,ANNOTATION_FIELD_DEF,
081 *        ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF
082 *        &quot;/&gt;
083 * &lt;/module&gt;
084 * </pre>
085 * @author Travis Schneeberger
086 */
087public class SuppressWarningsCheck extends AbstractFormatCheck
088{
089    /** {@link SuppressWarnings SuppressWarnings} annotation name */
090    private static final String SUPPRESS_WARNINGS = "SuppressWarnings";
091
092    /**
093     * fully-qualified {@link SuppressWarnings SuppressWarnings}
094     * annotation name
095     */
096    private static final String FQ_SUPPRESS_WARNINGS =
097        "java.lang." + SUPPRESS_WARNINGS;
098
099    /**
100     * A key is pointing to the warning message text in "messages.properties"
101     * file.
102     */
103    public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED =
104        "suppressed.warning.not.allowed";
105
106    /**
107     * Ctor that specifies the default for the format property
108     * as specified in the class javadocs.
109     */
110    public SuppressWarningsCheck()
111    {
112        super("^$|^\\s+$");
113    }
114
115    /** {@inheritDoc} */
116    @Override
117    public final int[] getDefaultTokens()
118    {
119        return this.getAcceptableTokens();
120    }
121
122    /** {@inheritDoc} */
123    @Override
124    public final int[] getAcceptableTokens()
125    {
126        return new int[] {
127            TokenTypes.CLASS_DEF,
128            TokenTypes.INTERFACE_DEF,
129            TokenTypes.ENUM_DEF,
130            TokenTypes.ANNOTATION_DEF,
131            TokenTypes.ANNOTATION_FIELD_DEF,
132            TokenTypes.ENUM_CONSTANT_DEF,
133            TokenTypes.PARAMETER_DEF,
134            TokenTypes.VARIABLE_DEF,
135            TokenTypes.METHOD_DEF,
136            TokenTypes.CTOR_DEF,
137        };
138    }
139
140    /** {@inheritDoc} */
141    @Override
142    public void visitToken(final DetailAST ast)
143    {
144        final DetailAST annotation = this.getSuppressWarnings(ast);
145
146        if (annotation == null) {
147            return;
148        }
149
150        final DetailAST warningHolder =
151            this.findWarningsHolder(annotation);
152
153        DetailAST warning = warningHolder.findFirstToken(TokenTypes.EXPR);
154
155        //rare case with empty array ex: @SuppressWarnings({})
156        if (warning == null) {
157            //check to see if empty warnings are forbidden -- are by default
158            this.logMatch(warningHolder.getLineNo(),
159                warningHolder.getColumnNo(), "");
160            return;
161        }
162
163        while (warning != null) {
164            if (warning.getType() == TokenTypes.EXPR) {
165                final DetailAST fChild = warning.getFirstChild();
166                switch (fChild.getType()) {
167                    //typical case
168                    case TokenTypes.STRING_LITERAL:
169                        final String warningText =
170                            this.removeQuotes(warning.getFirstChild().getText());
171                        this.logMatch(warning.getLineNo(),
172                                warning.getColumnNo(), warningText);
173                        break;
174                        //conditional case
175                        //ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
176                    case TokenTypes.QUESTION:
177                        this.walkConditional(fChild);
178                        break;
179                        //param in constant case
180                        //ex: public static final String UNCHECKED = "unchecked";
181                        //@SuppressWarnings(UNCHECKED) or @SuppressWarnings(SomeClass.UNCHECKED)
182                    case TokenTypes.IDENT:
183                    case TokenTypes.DOT:
184                        break;
185                    default:
186                        throw new IllegalStateException("Should never get here, type: "
187                                + fChild.getType() + " text: " + fChild.getText());
188                }
189            }
190            warning = warning.getNextSibling();
191        }
192    }
193
194    /**
195     * Gets the {@link SuppressWarnings SuppressWarnings} annotation
196     * that is annotating the AST.  If the annotation does not exist
197     * this method will return {@code null}.
198     *
199     * @param ast the AST
200     * @return the {@link SuppressWarnings SuppressWarnings} annotation
201     */
202    private DetailAST getSuppressWarnings(DetailAST ast)
203    {
204        final DetailAST annotation = AnnotationUtility.getAnnotation(
205            ast, SuppressWarningsCheck.SUPPRESS_WARNINGS);
206
207        return (annotation != null) ? annotation
208            : AnnotationUtility.getAnnotation(
209                ast, SuppressWarningsCheck.FQ_SUPPRESS_WARNINGS);
210    }
211
212    /**
213     * This method looks for a warning that matches a configured expression.
214     * If found it logs a violation at the given line and column number.
215     *
216     * @param lineNo the line number
217     * @param colNum the column number
218     * @param warningText the warning.
219     */
220    private void logMatch(final int lineNo,
221        final int colNum, final String warningText)
222    {
223        final Matcher matcher = this.getRegexp().matcher(warningText);
224        if (matcher.matches()) {
225            this.log(lineNo, colNum,
226                    MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText);
227        }
228    }
229
230    /**
231     * Find the parent (holder) of the of the warnings (Expr).
232     *
233     * @param annotation the annotation
234     * @return a Token representing the expr.
235     */
236    private DetailAST findWarningsHolder(final DetailAST annotation)
237    {
238        final DetailAST annValuePair =
239            annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
240        final DetailAST annArrayInit;
241
242        if (annValuePair != null) {
243            annArrayInit =
244                annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
245        }
246        else {
247            annArrayInit =
248                annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
249        }
250
251        if (annArrayInit != null) {
252            return annArrayInit;
253        }
254
255        return annotation;
256    }
257
258    /**
259     * Strips a single double quote from the front and back of a string.
260     *
261     * For example:
262     * <br/>
263     * Input String = "unchecked"
264     * <br/>
265     * Output String = unchecked
266     *
267     * @param warning the warning string
268     * @return the string without two quotes
269     */
270    private String removeQuotes(final String warning)
271    {
272        assert warning != null : "the warning was null";
273        assert warning.charAt(0) == '"';
274        assert warning.charAt(warning.length() - 1) == '"';
275
276        return warning.substring(1, warning.length() - 1);
277    }
278
279    /**
280     * Recursively walks a conditional expression checking the left
281     * and right sides, checking for matches and
282     * logging violations.
283     *
284     * @param cond a Conditional type
285     * {@link TokenTypes#QUESTION QUESTION}
286     */
287    private void walkConditional(final DetailAST cond)
288    {
289        if (cond.getType() != TokenTypes.QUESTION) {
290            final String warningText =
291                this.removeQuotes(cond.getText());
292            this.logMatch(cond.getLineNo(), cond.getColumnNo(), warningText);
293            return;
294        }
295
296        this.walkConditional(this.getCondLeft(cond));
297        this.walkConditional(this.getCondRight(cond));
298    }
299
300    /**
301     * Retrieves the left side of a conditional.
302     *
303     * @param cond cond a conditional type
304     * {@link TokenTypes#QUESTION QUESTION}
305     * @return either the value
306     * or another conditional
307     */
308    private DetailAST getCondLeft(final DetailAST cond)
309    {
310        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
311        return colon.getPreviousSibling();
312    }
313
314    /**
315     * Retrieves the right side of a conditional.
316     *
317     * @param cond a conditional type
318     * {@link TokenTypes#QUESTION QUESTION}
319     * @return either the value
320     * or another conditional
321     */
322    private DetailAST getCondRight(final DetailAST cond)
323    {
324        final DetailAST colon = cond.findFirstToken(TokenTypes.COLON);
325        return colon.getNextSibling();
326    }
327}