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     * Ctor that specifies the default for the format property
101     * as specified in the class javadocs.
102     */
103    public SuppressWarningsCheck()
104    {
105        super("^$|^\\s+$");
106    }
107
108    /** {@inheritDoc} */
109    @Override
110    public final int[] getDefaultTokens()
111    {
112        return this.getAcceptableTokens();
113    }
114
115    /** {@inheritDoc} */
116    @Override
117    public final int[] getAcceptableTokens()
118    {
119        return new int[] {
120            TokenTypes.CLASS_DEF,
121            TokenTypes.INTERFACE_DEF,
122            TokenTypes.ENUM_DEF,
123            TokenTypes.ANNOTATION_DEF,
124            TokenTypes.ANNOTATION_FIELD_DEF,
125            TokenTypes.ENUM_CONSTANT_DEF,
126            TokenTypes.PARAMETER_DEF,
127            TokenTypes.VARIABLE_DEF,
128            TokenTypes.METHOD_DEF,
129            TokenTypes.CTOR_DEF,
130        };
131    }
132
133    /** {@inheritDoc} */
134    @Override
135    public void visitToken(final DetailAST aAST)
136    {
137        final DetailAST annotation = this.getSuppressWarnings(aAST);
138
139        if (annotation == null) {
140            return;
141        }
142
143        final DetailAST warningHolder =
144            this.findWarningsHolder(annotation);
145
146        DetailAST warning = warningHolder.findFirstToken(TokenTypes.EXPR);
147
148        //rare case with empty array ex: @SuppressWarnings({})
149        if (warning == null) {
150            //check to see if empty warnings are forbidden -- are by default
151            this.logMatch(warningHolder.getLineNo(),
152                warningHolder.getColumnNo(), "");
153            return;
154        }
155
156        while (warning != null) {
157            if (warning.getType() == TokenTypes.EXPR) {
158                final DetailAST fChild = warning.getFirstChild();
159
160                //typical case
161                if (fChild.getType() == TokenTypes.STRING_LITERAL) {
162                    final String warningText =
163                        this.removeQuotes(warning.getFirstChild().getText());
164                    this.logMatch(warning.getLineNo(),
165                        warning.getColumnNo(), warningText);
166
167     //conditional case
168     //ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")
169                }
170                else if (fChild.getType() == TokenTypes.QUESTION) {
171                    this.walkConditional(fChild);
172                }
173                else {
174                    assert false : "Should never get here, type: "
175                        + fChild.getType() + " text: " + fChild.getText();
176                }
177            }
178            warning = warning.getNextSibling();
179        }
180    }
181
182    /**
183     * Gets the {@link SuppressWarnings SuppressWarnings} annotation
184     * that is annotating the AST.  If the annotation does not exist
185     * this method will return {@code null}.
186     *
187     * @param aAST the AST
188     * @return the {@link SuppressWarnings SuppressWarnings} annotation
189     */
190    private DetailAST getSuppressWarnings(DetailAST aAST)
191    {
192        final DetailAST annotation = AnnotationUtility.getAnnotation(
193            aAST, SuppressWarningsCheck.SUPPRESS_WARNINGS);
194
195        return (annotation != null) ? annotation
196            : AnnotationUtility.getAnnotation(
197                aAST, SuppressWarningsCheck.FQ_SUPPRESS_WARNINGS);
198    }
199
200    /**
201     * This method looks for a warning that matches a configured expression.
202     * If found it logs a violation at the given line and column number.
203     *
204     * @param aLineNo the line number
205     * @param aColNum the column number
206     * @param aWarningText the warning.
207     */
208    private void logMatch(final int aLineNo,
209        final int aColNum, final String aWarningText)
210    {
211        final Matcher matcher = this.getRegexp().matcher(aWarningText);
212        if (matcher.matches()) {
213            this.log(aLineNo, aColNum,
214                "suppressed.warning.not.allowed", aWarningText);
215        }
216    }
217
218    /**
219     * Find the parent (holder) of the of the warnings (Expr).
220     *
221     * @param aAnnotation the annotation
222     * @return a Token representing the expr.
223     */
224    private DetailAST findWarningsHolder(final DetailAST aAnnotation)
225    {
226        final DetailAST annValuePair =
227            aAnnotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
228        final DetailAST annArrayInit;
229
230        if (annValuePair != null) {
231            annArrayInit =
232                annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
233        }
234        else {
235            annArrayInit =
236                aAnnotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
237        }
238
239        if (annArrayInit != null) {
240            return annArrayInit;
241        }
242
243        return aAnnotation;
244    }
245
246    /**
247     * Strips a single double quote from the front and back of a string.
248     *
249     * For example:
250     * <br/>
251     * Input String = "unchecked"
252     * <br/>
253     * Output String = unchecked
254     *
255     * @param aWarning the warning string
256     * @return the string without two quotes
257     */
258    private String removeQuotes(final String aWarning)
259    {
260        assert aWarning != null : "the aWarning was null";
261        assert aWarning.charAt(0) == '"';
262        assert aWarning.charAt(aWarning.length() - 1) == '"';
263
264        return aWarning.substring(1, aWarning.length() - 1);
265    }
266
267    /**
268     * Recursively walks a conditional expression checking the left
269     * and right sides, checking for matches and
270     * logging violations.
271     *
272     * @param aCond a Conditional type
273     * {@link TokenTypes#QUESTION QUESTION}
274     */
275    private void walkConditional(final DetailAST aCond)
276    {
277        if (aCond.getType() != TokenTypes.QUESTION) {
278            final String warningText =
279                this.removeQuotes(aCond.getText());
280            this.logMatch(aCond.getLineNo(), aCond.getColumnNo(), warningText);
281            return;
282        }
283
284        this.walkConditional(this.getCondLeft(aCond));
285        this.walkConditional(this.getCondRight(aCond));
286    }
287
288    /**
289     * Retrieves the left side of a conditional.
290     *
291     * @param aCond aCond a conditional type
292     * {@link TokenTypes#QUESTION QUESTION}
293     * @return either the value
294     * or another conditional
295     */
296    private DetailAST getCondLeft(final DetailAST aCond)
297    {
298        final DetailAST colon = aCond.findFirstToken(TokenTypes.COLON);
299        return colon.getPreviousSibling();
300    }
301
302    /**
303     * Retrieves the right side of a conditional.
304     *
305     * @param aCond a conditional type
306     * {@link TokenTypes#QUESTION QUESTION}
307     * @return either the value
308     * or another conditional
309     */
310    private DetailAST getCondRight(final DetailAST aCond)
311    {
312        final DetailAST colon = aCond.findFirstToken(TokenTypes.COLON);
313        return colon.getNextSibling();
314    }
315}