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 * 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     *
184     * @param exceptionVariablePattern
185     *        pattern of exception's variable name.
186     */
187    public void setExceptionVariableName(Pattern exceptionVariablePattern) {
188        exceptionVariableName = exceptionVariablePattern;
189    }
190
191    /**
192     * Setter to specify the RegExp for the first comment inside empty catch block.
193     * If check meets comment inside empty catch block matching specified format - empty
194     * block is suppressed. If it is multi-line comment - only its first line is analyzed.
195     *
196     * @param commentPattern
197     *        pattern of comment.
198     */
199    public void setCommentFormat(Pattern commentPattern) {
200        commentFormat = commentPattern;
201    }
202
203    @Override
204    public int[] getDefaultTokens() {
205        return getRequiredTokens();
206    }
207
208    @Override
209    public int[] getAcceptableTokens() {
210        return getRequiredTokens();
211    }
212
213    @Override
214    public int[] getRequiredTokens() {
215        return new int[] {
216            TokenTypes.LITERAL_CATCH,
217        };
218    }
219
220    @Override
221    public boolean isCommentNodesRequired() {
222        return true;
223    }
224
225    @Override
226    public void visitToken(DetailAST ast) {
227        visitCatchBlock(ast);
228    }
229
230    /**
231     * Visits catch ast node, if it is empty catch block - checks it according to
232     *  Check's options. If exception's variable name or comment inside block are matching
233     *   specified regexp - skips from consideration, else - puts violation.
234     *
235     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
236     */
237    private void visitCatchBlock(DetailAST catchAst) {
238        if (isEmptyCatchBlock(catchAst)) {
239            final String commentContent = getCommentFirstLine(catchAst);
240            if (isVerifiable(catchAst, commentContent)) {
241                log(catchAst.findFirstToken(TokenTypes.SLIST), MSG_KEY_CATCH_BLOCK_EMPTY);
242            }
243        }
244    }
245
246    /**
247     * Gets the first line of comment in catch block. If comment is single-line -
248     *  returns it fully, else if comment is multi-line - returns the first line.
249     *
250     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
251     * @return the first line of comment in catch block, "" if no comment was found.
252     */
253    private static String getCommentFirstLine(DetailAST catchAst) {
254        final DetailAST slistToken = catchAst.getLastChild();
255        final DetailAST firstElementInBlock = slistToken.getFirstChild();
256        String commentContent = "";
257        if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
258            commentContent = firstElementInBlock.getFirstChild().getText();
259        }
260        else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
261            commentContent = firstElementInBlock.getFirstChild().getText();
262            final String[] lines = LINE_END_PATTERN.split(commentContent);
263            for (String line : lines) {
264                if (!line.isEmpty()) {
265                    commentContent = line;
266                    break;
267                }
268            }
269        }
270        return commentContent;
271    }
272
273    /**
274     * Checks if current empty catch block is verifiable according to Check's options
275     *  (exception's variable name and comment format are both in consideration).
276     *
277     * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
278     * @param commentContent text of comment.
279     * @return true if empty catch block is verifiable by Check.
280     */
281    private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
282        final String variableName = getExceptionVariableName(emptyCatchAst);
283        final boolean isMatchingVariableName = exceptionVariableName
284                .matcher(variableName).find();
285        final boolean isMatchingCommentContent = !commentContent.isEmpty()
286                 && commentFormat.matcher(commentContent).find();
287        return !isMatchingVariableName && !isMatchingCommentContent;
288    }
289
290    /**
291     * Checks if catch block is empty or contains only comments.
292     *
293     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
294     * @return true if catch block is empty.
295     */
296    private static boolean isEmptyCatchBlock(DetailAST catchAst) {
297        boolean result = true;
298        final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
299        DetailAST catchBlockStmt = slistToken.getFirstChild();
300        while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
301            if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
302                 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
303                result = false;
304                break;
305            }
306            catchBlockStmt = catchBlockStmt.getNextSibling();
307        }
308        return result;
309    }
310
311    /**
312     * Gets variable's name associated with exception.
313     *
314     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
315     * @return Variable's name associated with exception.
316     */
317    private static String getExceptionVariableName(DetailAST catchAst) {
318        final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
319        final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
320        return variableName.getText();
321    }
322
323}