001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.CommonUtil;
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 */
127@StatelessCheck
128public class EmptyCatchBlockCheck extends AbstractCheck {
129
130    /**
131     * A key is pointing to the warning message text in "messages.properties"
132     * file.
133     */
134    public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty";
135
136    /** Format of skipping exception's variable name. */
137    private String exceptionVariableName = "^$";
138
139    /** Format of comment. */
140    private String commentFormat = ".*";
141
142    /**
143     * Regular expression pattern compiled from exception's variable name.
144     */
145    private Pattern variableNameRegexp = Pattern.compile(exceptionVariableName);
146
147    /**
148     * Regular expression pattern compiled from comment's format.
149     */
150    private Pattern commentRegexp = Pattern.compile(commentFormat);
151
152    /**
153     * Setter for exception's variable name format.
154     * @param exceptionVariableName
155     *        format of exception's variable name.
156     * @throws org.apache.commons.beanutils.ConversionException
157     *         if unable to create Pattern object.
158     */
159    public void setExceptionVariableName(String exceptionVariableName) {
160        this.exceptionVariableName = exceptionVariableName;
161        variableNameRegexp = CommonUtil.createPattern(exceptionVariableName);
162    }
163
164    /**
165     * Setter for comment format.
166     * @param commentFormat
167     *        format of comment.
168     * @throws org.apache.commons.beanutils.ConversionException
169     *         if unable to create Pattern object.
170     */
171    public void setCommentFormat(String commentFormat) {
172        this.commentFormat = commentFormat;
173        commentRegexp = CommonUtil.createPattern(commentFormat);
174    }
175
176    @Override
177    public int[] getDefaultTokens() {
178        return getRequiredTokens();
179    }
180
181    @Override
182    public int[] getAcceptableTokens() {
183        return getRequiredTokens();
184    }
185
186    @Override
187    public int[] getRequiredTokens() {
188        return new int[] {
189            TokenTypes.LITERAL_CATCH,
190        };
191    }
192
193    @Override
194    public boolean isCommentNodesRequired() {
195        return true;
196    }
197
198    @Override
199    public void visitToken(DetailAST ast) {
200        visitCatchBlock(ast);
201    }
202
203    /**
204     * Visits catch ast node, if it is empty catch block - checks it according to
205     *  Check's options. If exception's variable name or comment inside block are matching
206     *   specified regexp - skips from consideration, else - puts violation.
207     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
208     */
209    private void visitCatchBlock(DetailAST catchAst) {
210        if (isEmptyCatchBlock(catchAst)) {
211            final String commentContent = getCommentFirstLine(catchAst);
212            if (isVerifiable(catchAst, commentContent)) {
213                log(catchAst.getLineNo(), MSG_KEY_CATCH_BLOCK_EMPTY);
214            }
215        }
216    }
217
218    /**
219     * Gets the first line of comment in catch block. If comment is single-line -
220     *  returns it fully, else if comment is multi-line - returns the first line.
221     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
222     * @return the first line of comment in catch block, "" if no comment was found.
223     */
224    private static String getCommentFirstLine(DetailAST catchAst) {
225        final DetailAST slistToken = catchAst.getLastChild();
226        final DetailAST firstElementInBlock = slistToken.getFirstChild();
227        String commentContent = "";
228        if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
229            commentContent = firstElementInBlock.getFirstChild().getText();
230        }
231        else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
232            commentContent = firstElementInBlock.getFirstChild().getText();
233            final String[] lines = commentContent.split(System.getProperty("line.separator"));
234            for (String line : lines) {
235                if (!line.isEmpty()) {
236                    commentContent = line;
237                    break;
238                }
239            }
240        }
241        return commentContent;
242    }
243
244    /**
245     * Checks if current empty catch block is verifiable according to Check's options
246     *  (exception's variable name and comment format are both in consideration).
247     * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
248     * @param commentContent text of comment.
249     * @return true if empty catch block is verifiable by Check.
250     */
251    private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
252        final String variableName = getExceptionVariableName(emptyCatchAst);
253        final boolean isMatchingVariableName = variableNameRegexp
254                .matcher(variableName).find();
255        final boolean isMatchingCommentContent = !commentContent.isEmpty()
256                 && commentRegexp.matcher(commentContent).find();
257        return !isMatchingVariableName && !isMatchingCommentContent;
258    }
259
260    /**
261     * Checks if catch block is empty or contains only comments.
262     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
263     * @return true if catch block is empty.
264     */
265    private static boolean isEmptyCatchBlock(DetailAST catchAst) {
266        boolean result = true;
267        final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
268        DetailAST catchBlockStmt = slistToken.getFirstChild();
269        while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
270            if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
271                 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
272                result = false;
273                break;
274            }
275            catchBlockStmt = catchBlockStmt.getNextSibling();
276        }
277        return result;
278    }
279
280    /**
281     * Gets variable's name associated with exception.
282     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
283     * @return Variable's name associated with exception.
284     */
285    private static String getExceptionVariableName(DetailAST catchAst) {
286        final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
287        final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
288        return variableName.getText();
289    }
290
291}