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