1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.blocks;
21  
22  import java.util.regex.Pattern;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  
29  /**
30   * <p>
31   * Checks for empty catch blocks.
32   * By default check allows empty catch block with any comment inside.
33   * </p>
34   * <p>
35   * There are two options to make validation more precise: <b>exceptionVariableName</b> and
36   * <b>commentFormat</b>.
37   * If both options are specified - they are applied by <b>any of them is matching</b>.
38   * </p>
39   * <ul>
40   * <li>
41   * Property {@code exceptionVariableName} - Specify the RegExp for the name of the variable
42   * associated with exception. If check meets variable name matching specified value - empty
43   * block is suppressed.
44   * Default value is {@code "^$" (empty)}.
45   * </li>
46   * <li>
47   * Property {@code commentFormat} - Specify the RegExp for the first comment inside empty
48   * catch block. If check meets comment inside empty catch block matching specified format
49   * - empty block is suppressed. If it is multi-line comment - only its first line is analyzed.
50   * Default value is {@code ".*"}.
51   * </li>
52   * </ul>
53   * <p>
54   * To configure the check to suppress empty catch block if exception's variable name is
55   * {@code expected} or {@code ignore} or there's any comment inside:
56   * </p>
57   * <pre>
58   * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
59   *   &lt;property name=&quot;exceptionVariableName&quot; value=&quot;expected|ignore&quot;/&gt;
60   * &lt;/module&gt;
61   * </pre>
62   * <p>
63   * Such empty blocks would be both suppressed:
64   * </p>
65   * <pre>
66   * try {
67   *   throw new RuntimeException();
68   * } catch (RuntimeException expected) {
69   * }
70   * try {
71   *   throw new RuntimeException();
72   * } catch (RuntimeException ignore) {
73   * }
74   * </pre>
75   * <p>
76   * To configure the check to suppress empty catch block if single-line comment inside
77   * is &quot;//This is expected&quot;:
78   * </p>
79   * <pre>
80   * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
81   *   &lt;property name=&quot;commentFormat&quot; value=&quot;This is expected&quot;/&gt;
82   * &lt;/module&gt;
83   * </pre>
84   * <p>
85   * Such empty block would be suppressed:
86   * </p>
87   * <pre>
88   * try {
89   *   throw new RuntimeException();
90   * } catch (RuntimeException ex) {
91   *   //This is expected
92   * }
93   * </pre>
94   * <p>
95   * To configure the check to suppress empty catch block if single-line comment inside
96   * is &quot;//This is expected&quot; or exception's
97   * variable name is &quot;myException&quot; (any option is matching):
98   * </p>
99   * <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
154 public 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      * Specify the RegExp for the name of the variable associated with exception.
164      * If check meets variable name matching specified value - empty block is suppressed.
165      */
166     private Pattern exceptionVariableName = Pattern.compile("^$");
167 
168     /**
169      * Specify the RegExp for the first comment inside empty catch block.
170      * If check meets comment inside empty catch block matching specified format - empty
171      * block is suppressed. If it is multi-line comment - only its first line is analyzed.
172      */
173     private Pattern commentFormat = Pattern.compile(".*");
174 
175     /**
176      * Setter to specify the RegExp for the name of the variable associated with exception.
177      * If check meets variable name matching specified value - empty block is suppressed.
178      * @param exceptionVariablePattern
179      *        pattern of exception's variable name.
180      */
181     public void setExceptionVariableName(Pattern exceptionVariablePattern) {
182         exceptionVariableName = exceptionVariablePattern;
183     }
184 
185     /**
186      * Setter to specify the RegExp for the first comment inside empty catch block.
187      * If check meets comment inside empty catch block matching specified format - empty
188      * block is suppressed. If it is multi-line comment - only its first line is analyzed.
189      * @param commentPattern
190      *        pattern of comment.
191      */
192     public void setCommentFormat(Pattern commentPattern) {
193         commentFormat = commentPattern;
194     }
195 
196     @Override
197     public int[] getDefaultTokens() {
198         return getRequiredTokens();
199     }
200 
201     @Override
202     public int[] getAcceptableTokens() {
203         return getRequiredTokens();
204     }
205 
206     @Override
207     public int[] getRequiredTokens() {
208         return new int[] {
209             TokenTypes.LITERAL_CATCH,
210         };
211     }
212 
213     @Override
214     public boolean isCommentNodesRequired() {
215         return true;
216     }
217 
218     @Override
219     public void visitToken(DetailAST ast) {
220         visitCatchBlock(ast);
221     }
222 
223     /**
224      * Visits catch ast node, if it is empty catch block - checks it according to
225      *  Check's options. If exception's variable name or comment inside block are matching
226      *   specified regexp - skips from consideration, else - puts violation.
227      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
228      */
229     private void visitCatchBlock(DetailAST catchAst) {
230         if (isEmptyCatchBlock(catchAst)) {
231             final String commentContent = getCommentFirstLine(catchAst);
232             if (isVerifiable(catchAst, commentContent)) {
233                 log(catchAst.getLineNo(), MSG_KEY_CATCH_BLOCK_EMPTY);
234             }
235         }
236     }
237 
238     /**
239      * Gets the first line of comment in catch block. If comment is single-line -
240      *  returns it fully, else if comment is multi-line - returns the first line.
241      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
242      * @return the first line of comment in catch block, "" if no comment was found.
243      */
244     private static String getCommentFirstLine(DetailAST catchAst) {
245         final DetailAST slistToken = catchAst.getLastChild();
246         final DetailAST firstElementInBlock = slistToken.getFirstChild();
247         String commentContent = "";
248         if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
249             commentContent = firstElementInBlock.getFirstChild().getText();
250         }
251         else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
252             commentContent = firstElementInBlock.getFirstChild().getText();
253             final String[] lines = commentContent.split(System.getProperty("line.separator"));
254             for (String line : lines) {
255                 if (!line.isEmpty()) {
256                     commentContent = line;
257                     break;
258                 }
259             }
260         }
261         return commentContent;
262     }
263 
264     /**
265      * Checks if current empty catch block is verifiable according to Check's options
266      *  (exception's variable name and comment format are both in consideration).
267      * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
268      * @param commentContent text of comment.
269      * @return true if empty catch block is verifiable by Check.
270      */
271     private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
272         final String variableName = getExceptionVariableName(emptyCatchAst);
273         final boolean isMatchingVariableName = exceptionVariableName
274                 .matcher(variableName).find();
275         final boolean isMatchingCommentContent = !commentContent.isEmpty()
276                  && commentFormat.matcher(commentContent).find();
277         return !isMatchingVariableName && !isMatchingCommentContent;
278     }
279 
280     /**
281      * Checks if catch block is empty or contains only comments.
282      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
283      * @return true if catch block is empty.
284      */
285     private static boolean isEmptyCatchBlock(DetailAST catchAst) {
286         boolean result = true;
287         final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
288         DetailAST catchBlockStmt = slistToken.getFirstChild();
289         while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
290             if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
291                  && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
292                 result = false;
293                 break;
294             }
295             catchBlockStmt = catchBlockStmt.getNextSibling();
296         }
297         return result;
298     }
299 
300     /**
301      * Gets variable's name associated with exception.
302      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
303      * @return Variable's name associated with exception.
304      */
305     private static String getExceptionVariableName(DetailAST catchAst) {
306         final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
307         final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
308         return variableName.getText();
309     }
310 
311 }