View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
29  
30  /**
31   * <p>
32   * Checks for empty catch blocks. There are two options to make validation more precise:
33   * </p>
34   *
35   * <p><b>exceptionVariableName</b> - the name of variable associated with exception,
36   * if Check meets variable name matching specified value - empty block is suppressed.<br>
37   *  default value: &quot;^$&quot;
38   * </p>
39   *
40   * <p><b>commentFormat</b> - the format of the first comment inside empty catch
41   * block, if Check meets comment inside empty catch block matching specified format
42   *  - empty block is suppressed. If it is multi-line comment - only its first line is analyzed.<br>
43   * default value: &quot;.*&quot;<br>
44   * So, by default Check allows empty catch block with any comment inside.
45   * </p>
46   * <p>
47   * If both options are specified - they are applied by <b>any of them is matching</b>.
48   * </p>
49   * Examples:
50   * <p>
51   * To configure the Check to suppress empty catch block if exception's variable name is
52   *  <b>expected</b> or <b>ignore</b>:
53   * </p>
54   * <pre>
55   * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
56   *    &lt;property name=&quot;exceptionVariableName&quot; value=&quot;ignore|expected;/&gt;
57   * &lt;/module&gt;
58   * </pre>
59   *
60   * <p>Such empty blocks would be both suppressed:<br>
61   * </p>
62   * <pre>
63   * {@code
64   * try {
65   *     throw new RuntimeException();
66   * } catch (RuntimeException expected) {
67   * }
68   * }
69   * {@code
70   * try {
71   *     throw new RuntimeException();
72   * } catch (RuntimeException ignore) {
73   * }
74   * }
75   * </pre>
76   * <p>
77   * To configure the Check to suppress empty catch block if single-line comment inside
78   *  is &quot;//This is expected&quot;:
79   * </p>
80   * <pre>
81   * &lt;module name=&quot;EmptyCatchBlock&quot;&gt;
82   *    &lt;property name=&quot;commentFormat&quot; value=&quot;This is expected&quot;/&gt;
83   * &lt;/module&gt;
84   * </pre>
85   *
86   * <p>Such empty block would be suppressed:<br>
87   * </p>
88   * <pre>
89   * {@code
90   * try {
91   *     throw new RuntimeException();
92   * } catch (RuntimeException ex) {
93   *     //This is expected
94   * }
95   * }
96   * </pre>
97   * <p>
98   * To configure the Check to suppress empty catch block if single-line comment inside
99   *  is &quot;//This is expected&quot; or exception's variable name is &quot;myException&quot;:
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  *
108  * <p>Such empty blocks would be both suppressed:<br>
109  * </p>
110  * <pre>
111  * {@code
112  * try {
113  *     throw new RuntimeException();
114  * } catch (RuntimeException ex) {
115  *     //This is expected
116  * }
117  * }
118  * {@code
119  * try {
120  *     throw new RuntimeException();
121  * } catch (RuntimeException myException) {
122  *
123  * }
124  * }
125  * </pre>
126  * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
127  */
128 @StatelessCheck
129 public class EmptyCatchBlockCheck extends AbstractCheck {
130 
131     /**
132      * A key is pointing to the warning message text in "messages.properties"
133      * file.
134      */
135     public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty";
136 
137     /** Format of skipping exception's variable name. */
138     private String exceptionVariableName = "^$";
139 
140     /** Format of comment. */
141     private String commentFormat = ".*";
142 
143     /**
144      * Regular expression pattern compiled from exception's variable name.
145      */
146     private Pattern variableNameRegexp = Pattern.compile(exceptionVariableName);
147 
148     /**
149      * Regular expression pattern compiled from comment's format.
150      */
151     private Pattern commentRegexp = Pattern.compile(commentFormat);
152 
153     /**
154      * Setter for exception's variable name format.
155      * @param exceptionVariableName
156      *        format of exception's variable name.
157      * @throws org.apache.commons.beanutils.ConversionException
158      *         if unable to create Pattern object.
159      */
160     public void setExceptionVariableName(String exceptionVariableName) {
161         this.exceptionVariableName = exceptionVariableName;
162         variableNameRegexp = CommonUtils.createPattern(exceptionVariableName);
163     }
164 
165     /**
166      * Setter for comment format.
167      * @param commentFormat
168      *        format of comment.
169      * @throws org.apache.commons.beanutils.ConversionException
170      *         if unable to create Pattern object.
171      */
172     public void setCommentFormat(String commentFormat) {
173         this.commentFormat = commentFormat;
174         commentRegexp = CommonUtils.createPattern(commentFormat);
175     }
176 
177     @Override
178     public int[] getDefaultTokens() {
179         return getRequiredTokens();
180     }
181 
182     @Override
183     public int[] getAcceptableTokens() {
184         return getRequiredTokens();
185     }
186 
187     @Override
188     public int[] getRequiredTokens() {
189         return new int[] {
190             TokenTypes.LITERAL_CATCH,
191         };
192     }
193 
194     @Override
195     public boolean isCommentNodesRequired() {
196         return true;
197     }
198 
199     @Override
200     public void visitToken(DetailAST ast) {
201         visitCatchBlock(ast);
202     }
203 
204     /**
205      * Visits catch ast node, if it is empty catch block - checks it according to
206      *  Check's options. If exception's variable name or comment inside block are matching
207      *   specified regexp - skips from consideration, else - puts violation.
208      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
209      */
210     private void visitCatchBlock(DetailAST catchAst) {
211         if (isEmptyCatchBlock(catchAst)) {
212             final String commentContent = getCommentFirstLine(catchAst);
213             if (isVerifiable(catchAst, commentContent)) {
214                 log(catchAst.getLineNo(), MSG_KEY_CATCH_BLOCK_EMPTY);
215             }
216         }
217     }
218 
219     /**
220      * Gets the first line of comment in catch block. If comment is single-line -
221      *  returns it fully, else if comment is multi-line - returns the first line.
222      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
223      * @return the first line of comment in catch block, "" if no comment was found.
224      */
225     private static String getCommentFirstLine(DetailAST catchAst) {
226         final DetailAST slistToken = catchAst.getLastChild();
227         final DetailAST firstElementInBlock = slistToken.getFirstChild();
228         String commentContent = "";
229         if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
230             commentContent = firstElementInBlock.getFirstChild().getText();
231         }
232         else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
233             commentContent = firstElementInBlock.getFirstChild().getText();
234             final String[] lines = commentContent.split(System.getProperty("line.separator"));
235             for (String line : lines) {
236                 if (!line.isEmpty()) {
237                     commentContent = line;
238                     break;
239                 }
240             }
241         }
242         return commentContent;
243     }
244 
245     /**
246      * Checks if current empty catch block is verifiable according to Check's options
247      *  (exception's variable name and comment format are both in consideration).
248      * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
249      * @param commentContent text of comment.
250      * @return true if empty catch block is verifiable by Check.
251      */
252     private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
253         final String variableName = getExceptionVariableName(emptyCatchAst);
254         final boolean isMatchingVariableName = variableNameRegexp
255                 .matcher(variableName).find();
256         final boolean isMatchingCommentContent = !commentContent.isEmpty()
257                  && commentRegexp.matcher(commentContent).find();
258         return !isMatchingVariableName && !isMatchingCommentContent;
259     }
260 
261     /**
262      * Checks if catch block is empty or contains only comments.
263      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
264      * @return true if catch block is empty.
265      */
266     private static boolean isEmptyCatchBlock(DetailAST catchAst) {
267         boolean result = true;
268         final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
269         DetailAST catchBlockStmt = slistToken.getFirstChild();
270         while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
271             if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
272                  && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
273                 result = false;
274                 break;
275             }
276             catchBlockStmt = catchBlockStmt.getNextSibling();
277         }
278         return result;
279     }
280 
281     /**
282      * Gets variable's name associated with exception.
283      * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
284      * @return Variable's name associated with exception.
285      */
286     private static String getExceptionVariableName(DetailAST catchAst) {
287         final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
288         final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
289         return variableName.getText();
290     }
291 
292 }