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