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