001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.api.AbstractCheck; 025import com.puppycrawl.tools.checkstyle.api.DetailAST; 026import com.puppycrawl.tools.checkstyle.api.TokenTypes; 027import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 028 029/** 030 * <p> 031 * Checks for empty catch blocks. There are two options to make validation more precise: 032 * </p> 033 * 034 * <p><b>exceptionVariableName</b> - the name of variable associated with exception, 035 * if Check meets variable name matching specified value - empty block is suppressed.<br> 036 * default value: "^$" 037 * </p> 038 * 039 * <p><b>commentFormat</b> - the format of the first comment inside empty catch 040 * block, if Check meets comment inside empty catch block matching specified format 041 * - empty block is suppressed. If it is multi-line comment - only its first line is analyzed.<br> 042 * default value: ".*"<br> 043 * So, by default Check allows empty catch block with any comment inside. 044 * </p> 045 * <p> 046 * If both options are specified - they are applied by <b>any of them is matching</b>. 047 * </p> 048 * Examples: 049 * <p> 050 * To configure the Check to suppress empty catch block if exception's variable name is 051 * <b>expected</b> or <b>ignore</b>: 052 * </p> 053 * <pre> 054 * <module name="EmptyCatchBlock"> 055 * <property name="exceptionVariableName" value="ignore|expected;/> 056 * </module> 057 * </pre> 058 * 059 * <p>Such empty blocks would be both suppressed:<br> 060 * </p> 061 * <pre> 062 * {@code 063 * try { 064 * throw new RuntimeException(); 065 * } catch (RuntimeException expected) { 066 * } 067 * } 068 * {@code 069 * try { 070 * throw new RuntimeException(); 071 * } catch (RuntimeException ignore) { 072 * } 073 * } 074 * </pre> 075 * <p> 076 * To configure the Check to suppress empty catch block if single-line comment inside 077 * is "//This is expected": 078 * </p> 079 * <pre> 080 * <module name="EmptyCatchBlock"> 081 * <property name="commentFormat" value="This is expected"/> 082 * </module> 083 * </pre> 084 * 085 * <p>Such empty block would be suppressed:<br> 086 * </p> 087 * <pre> 088 * {@code 089 * try { 090 * throw new RuntimeException(); 091 * } catch (RuntimeException ex) { 092 * //This is expected 093 * } 094 * } 095 * </pre> 096 * <p> 097 * To configure the Check to suppress empty catch block if single-line comment inside 098 * is "//This is expected" or exception's variable name is "myException": 099 * </p> 100 * <pre> 101 * <module name="EmptyCatchBlock"> 102 * <property name="commentFormat" value="This is expected"/> 103 * <property name="exceptionVariableName" value="myException"/> 104 * </module> 105 * </pre> 106 * 107 * <p>Such empty blocks would be both suppressed:<br> 108 * </p> 109 * <pre> 110 * {@code 111 * try { 112 * throw new RuntimeException(); 113 * } catch (RuntimeException ex) { 114 * //This is expected 115 * } 116 * } 117 * {@code 118 * try { 119 * throw new RuntimeException(); 120 * } catch (RuntimeException myException) { 121 * 122 * } 123 * } 124 * </pre> 125 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 126 */ 127public class EmptyCatchBlockCheck extends AbstractCheck { 128 129 /** 130 * A key is pointing to the warning message text in "messages.properties" 131 * file. 132 */ 133 public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty"; 134 135 /** Format of skipping exception's variable name. */ 136 private String exceptionVariableName = "^$"; 137 138 /** Format of comment. */ 139 private String commentFormat = ".*"; 140 141 /** 142 * Regular expression pattern compiled from exception's variable name. 143 */ 144 private Pattern variableNameRegexp = Pattern.compile(exceptionVariableName); 145 146 /** 147 * Regular expression pattern compiled from comment's format. 148 */ 149 private Pattern commentRegexp = Pattern.compile(commentFormat); 150 151 /** 152 * Setter for exception's variable name format. 153 * @param exceptionVariableName 154 * format of exception's variable name. 155 * @throws org.apache.commons.beanutils.ConversionException 156 * if unable to create Pattern object. 157 */ 158 public void setExceptionVariableName(String exceptionVariableName) { 159 this.exceptionVariableName = exceptionVariableName; 160 variableNameRegexp = CommonUtils.createPattern(exceptionVariableName); 161 } 162 163 /** 164 * Setter for comment format. 165 * @param commentFormat 166 * format of comment. 167 * @throws org.apache.commons.beanutils.ConversionException 168 * if unable to create Pattern object. 169 */ 170 public void setCommentFormat(String commentFormat) { 171 this.commentFormat = commentFormat; 172 commentRegexp = CommonUtils.createPattern(commentFormat); 173 } 174 175 @Override 176 public int[] getDefaultTokens() { 177 return getAcceptableTokens(); 178 } 179 180 @Override 181 public int[] getAcceptableTokens() { 182 return new int[] { 183 TokenTypes.LITERAL_CATCH, 184 }; 185 } 186 187 @Override 188 public int[] getRequiredTokens() { 189 return getAcceptableTokens(); 190 } 191 192 @Override 193 public boolean isCommentNodesRequired() { 194 return true; 195 } 196 197 @Override 198 public void visitToken(DetailAST ast) { 199 visitCatchBlock(ast); 200 } 201 202 /** 203 * Visits catch ast node, if it is empty catch block - checks it according to 204 * Check's options. If exception's variable name or comment inside block are matching 205 * specified regexp - skips from consideration, else - puts violation. 206 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 207 */ 208 private void visitCatchBlock(DetailAST catchAst) { 209 if (isEmptyCatchBlock(catchAst)) { 210 final String commentContent = getCommentFirstLine(catchAst); 211 if (isVerifiable(catchAst, commentContent)) { 212 log(catchAst.getLineNo(), MSG_KEY_CATCH_BLOCK_EMPTY); 213 } 214 } 215 } 216 217 /** 218 * Gets the first line of comment in catch block. If comment is single-line - 219 * returns it fully, else if comment is multi-line - returns the first line. 220 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 221 * @return the first line of comment in catch block, "" if no comment was found. 222 */ 223 private static String getCommentFirstLine(DetailAST catchAst) { 224 final DetailAST slistToken = catchAst.getLastChild(); 225 final DetailAST firstElementInBlock = slistToken.getFirstChild(); 226 String commentContent = ""; 227 if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 228 commentContent = firstElementInBlock.getFirstChild().getText(); 229 } 230 else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 231 commentContent = firstElementInBlock.getFirstChild().getText(); 232 final String[] lines = commentContent.split(System.getProperty("line.separator")); 233 for (String line : lines) { 234 if (!line.isEmpty()) { 235 commentContent = line; 236 break; 237 } 238 } 239 } 240 return commentContent; 241 } 242 243 /** 244 * Checks if current empty catch block is verifiable according to Check's options 245 * (exception's variable name and comment format are both in consideration). 246 * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block. 247 * @param commentContent text of comment. 248 * @return true if empty catch block is verifiable by Check. 249 */ 250 private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) { 251 final String variableName = getExceptionVariableName(emptyCatchAst); 252 final boolean isMatchingVariableName = variableNameRegexp 253 .matcher(variableName).find(); 254 final boolean isMatchingCommentContent = !commentContent.isEmpty() 255 && commentRegexp.matcher(commentContent).find(); 256 return !isMatchingVariableName && !isMatchingCommentContent; 257 } 258 259 /** 260 * Checks if catch block is empty or contains only comments. 261 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 262 * @return true if catch block is empty. 263 */ 264 private static boolean isEmptyCatchBlock(DetailAST catchAst) { 265 boolean result = true; 266 final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST); 267 DetailAST catchBlockStmt = slistToken.getFirstChild(); 268 while (catchBlockStmt.getType() != TokenTypes.RCURLY) { 269 if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT 270 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) { 271 result = false; 272 break; 273 } 274 catchBlockStmt = catchBlockStmt.getNextSibling(); 275 } 276 return result; 277 } 278 279 /** 280 * Gets variable's name associated with exception. 281 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 282 * @return Variable's name associated with exception. 283 */ 284 private static String getExceptionVariableName(DetailAST catchAst) { 285 final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF); 286 final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT); 287 return variableName.getText(); 288 } 289 290}