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