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