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.blocks;
021
022import java.util.regex.Pattern;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
029
030/**
031 * <p>
032 * Checks for empty catch blocks. There are two options to make validation more precise:
033 * </p>
034 *
035 * <p><b>exceptionVariableName</b> - the name of variable associated with exception,
036 * if Check meets variable name matching specified value - empty block is suppressed.<br>
037 *  default value: &quot;^$&quot;
038 * </p>
039 *
040 * <p><b>commentFormat</b> - the format of the first comment inside empty catch
041 * block, if Check meets comment inside empty catch block matching specified format
042 *  - empty block is suppressed. If it is multi-line comment - only its first line is analyzed.<br>
043 * default value: &quot;.*&quot;<br>
044 * So, by default Check allows empty catch block with any comment inside.
045 * </p>
046 * <p>
047 * If both options are specified - they are applied by <b>any of them is matching</b>.
048 * </p>
049 * Examples:
050 * <p>
051 * To configure the Check to suppress empty catch block if exception's variable name is
052 *  <b>expected</b> or <b>ignore</b>:
053 * </p>
054 * <pre>
055 * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
056 *    &lt;property name=&quot;exceptionVariableName&quot; value=&quot;ignore|expected;/&gt;
057 * &lt;/module&gt;
058 * </pre>
059 *
060 * <p>Such empty blocks would be both suppressed:<br>
061 * </p>
062 * <pre>
063 * {@code
064 * try {
065 *     throw new RuntimeException();
066 * } catch (RuntimeException expected) {
067 * }
068 * }
069 * {@code
070 * try {
071 *     throw new RuntimeException();
072 * } catch (RuntimeException ignore) {
073 * }
074 * }
075 * </pre>
076 * <p>
077 * To configure the Check to suppress empty catch block if single-line comment inside
078 *  is &quot;//This is expected&quot;:
079 * </p>
080 * <pre>
081 * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
082 *    &lt;property name=&quot;commentFormat&quot; value=&quot;This is expected&quot;/&gt;
083 * &lt;/module&gt;
084 * </pre>
085 *
086 * <p>Such empty block would be suppressed:<br>
087 * </p>
088 * <pre>
089 * {@code
090 * try {
091 *     throw new RuntimeException();
092 * } catch (RuntimeException ex) {
093 *     //This is expected
094 * }
095 * }
096 * </pre>
097 * <p>
098 * To configure the Check to suppress empty catch block if single-line comment inside
099 *  is &quot;//This is expected&quot; or exception's variable name is &quot;myException&quot;:
100 * </p>
101 * <pre>
102 * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
103 *    &lt;property name=&quot;commentFormat&quot; value=&quot;This is expected&quot;/&gt;
104 *    &lt;property name=&quot;exceptionVariableName&quot; value=&quot;myException&quot;/&gt;
105 * &lt;/module&gt;
106 * </pre>
107 *
108 * <p>Such empty blocks would be both suppressed:<br>
109 * </p>
110 * <pre>
111 * {@code
112 * try {
113 *     throw new RuntimeException();
114 * } catch (RuntimeException ex) {
115 *     //This is expected
116 * }
117 * }
118 * {@code
119 * try {
120 *     throw new RuntimeException();
121 * } catch (RuntimeException myException) {
122 *
123 * }
124 * }
125 * </pre>
126 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
127 */
128@StatelessCheck
129public class EmptyCatchBlockCheck extends AbstractCheck {
130
131    /**
132     * A key is pointing to the warning message text in "messages.properties"
133     * file.
134     */
135    public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty";
136
137    /** Format of skipping exception's variable name. */
138    private String exceptionVariableName = "^$";
139
140    /** Format of comment. */
141    private String commentFormat = ".*";
142
143    /**
144     * Regular expression pattern compiled from exception's variable name.
145     */
146    private Pattern variableNameRegexp = Pattern.compile(exceptionVariableName);
147
148    /**
149     * Regular expression pattern compiled from comment's format.
150     */
151    private Pattern commentRegexp = Pattern.compile(commentFormat);
152
153    /**
154     * Setter for exception's variable name format.
155     * @param exceptionVariableName
156     *        format of exception's variable name.
157     * @throws org.apache.commons.beanutils.ConversionException
158     *         if unable to create Pattern object.
159     */
160    public void setExceptionVariableName(String exceptionVariableName) {
161        this.exceptionVariableName = exceptionVariableName;
162        variableNameRegexp = CommonUtils.createPattern(exceptionVariableName);
163    }
164
165    /**
166     * Setter for comment format.
167     * @param commentFormat
168     *        format of comment.
169     * @throws org.apache.commons.beanutils.ConversionException
170     *         if unable to create Pattern object.
171     */
172    public void setCommentFormat(String commentFormat) {
173        this.commentFormat = commentFormat;
174        commentRegexp = CommonUtils.createPattern(commentFormat);
175    }
176
177    @Override
178    public int[] getDefaultTokens() {
179        return getRequiredTokens();
180    }
181
182    @Override
183    public int[] getAcceptableTokens() {
184        return getRequiredTokens();
185    }
186
187    @Override
188    public int[] getRequiredTokens() {
189        return new int[] {
190            TokenTypes.LITERAL_CATCH,
191        };
192    }
193
194    @Override
195    public boolean isCommentNodesRequired() {
196        return true;
197    }
198
199    @Override
200    public void visitToken(DetailAST ast) {
201        visitCatchBlock(ast);
202    }
203
204    /**
205     * Visits catch ast node, if it is empty catch block - checks it according to
206     *  Check's options. If exception's variable name or comment inside block are matching
207     *   specified regexp - skips from consideration, else - puts violation.
208     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
209     */
210    private void visitCatchBlock(DetailAST catchAst) {
211        if (isEmptyCatchBlock(catchAst)) {
212            final String commentContent = getCommentFirstLine(catchAst);
213            if (isVerifiable(catchAst, commentContent)) {
214                log(catchAst.getLineNo(), MSG_KEY_CATCH_BLOCK_EMPTY);
215            }
216        }
217    }
218
219    /**
220     * Gets the first line of comment in catch block. If comment is single-line -
221     *  returns it fully, else if comment is multi-line - returns the first line.
222     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
223     * @return the first line of comment in catch block, "" if no comment was found.
224     */
225    private static String getCommentFirstLine(DetailAST catchAst) {
226        final DetailAST slistToken = catchAst.getLastChild();
227        final DetailAST firstElementInBlock = slistToken.getFirstChild();
228        String commentContent = "";
229        if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
230            commentContent = firstElementInBlock.getFirstChild().getText();
231        }
232        else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
233            commentContent = firstElementInBlock.getFirstChild().getText();
234            final String[] lines = commentContent.split(System.getProperty("line.separator"));
235            for (String line : lines) {
236                if (!line.isEmpty()) {
237                    commentContent = line;
238                    break;
239                }
240            }
241        }
242        return commentContent;
243    }
244
245    /**
246     * Checks if current empty catch block is verifiable according to Check's options
247     *  (exception's variable name and comment format are both in consideration).
248     * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
249     * @param commentContent text of comment.
250     * @return true if empty catch block is verifiable by Check.
251     */
252    private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
253        final String variableName = getExceptionVariableName(emptyCatchAst);
254        final boolean isMatchingVariableName = variableNameRegexp
255                .matcher(variableName).find();
256        final boolean isMatchingCommentContent = !commentContent.isEmpty()
257                 && commentRegexp.matcher(commentContent).find();
258        return !isMatchingVariableName && !isMatchingCommentContent;
259    }
260
261    /**
262     * Checks if catch block is empty or contains only comments.
263     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
264     * @return true if catch block is empty.
265     */
266    private static boolean isEmptyCatchBlock(DetailAST catchAst) {
267        boolean result = true;
268        final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
269        DetailAST catchBlockStmt = slistToken.getFirstChild();
270        while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
271            if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
272                 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
273                result = false;
274                break;
275            }
276            catchBlockStmt = catchBlockStmt.getNextSibling();
277        }
278        return result;
279    }
280
281    /**
282     * Gets variable's name associated with exception.
283     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
284     * @return Variable's name associated with exception.
285     */
286    private static String getExceptionVariableName(DetailAST catchAst) {
287        final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
288        final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
289        return variableName.getText();
290    }
291
292}