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