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