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 * Type is {@code java.util.regex.Pattern}. 045 * Default value is {@code "^$" (empty)}. 046 * </li> 047 * <li> 048 * Property {@code commentFormat} - Specify the RegExp for the first comment inside empty 049 * catch block. If check meets comment inside empty catch block matching specified format 050 * - empty block is suppressed. If it is multi-line comment - only its first line is analyzed. 051 * Type is {@code java.util.regex.Pattern}. 052 * Default value is {@code ".*"}. 053 * </li> 054 * </ul> 055 * <p> 056 * To configure the check to suppress empty catch block if exception's variable name is 057 * {@code expected} or {@code ignore} or there's any comment inside: 058 * </p> 059 * <pre> 060 * <module name="EmptyCatchBlock"> 061 * <property name="exceptionVariableName" value="expected|ignore"/> 062 * </module> 063 * </pre> 064 * <p> 065 * Such empty blocks would be both suppressed: 066 * </p> 067 * <pre> 068 * try { 069 * throw new RuntimeException(); 070 * } catch (RuntimeException expected) { 071 * } 072 * try { 073 * throw new RuntimeException(); 074 * } catch (RuntimeException ignore) { 075 * } 076 * </pre> 077 * <p> 078 * To configure the check to suppress empty catch block if single-line comment inside 079 * is "//This is expected": 080 * </p> 081 * <pre> 082 * <module name="EmptyCatchBlock"> 083 * <property name="commentFormat" value="This is expected"/> 084 * </module> 085 * </pre> 086 * <p> 087 * Such empty block would be suppressed: 088 * </p> 089 * <pre> 090 * try { 091 * throw new RuntimeException(); 092 * } catch (RuntimeException ex) { 093 * //This is expected 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 099 * variable name is "myException" (any option is matching): 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 * <p> 108 * Such empty blocks would be suppressed: 109 * </p> 110 * <pre> 111 * try { 112 * throw new RuntimeException(); 113 * } catch (RuntimeException e) { 114 * //This is expected 115 * } 116 * ... 117 * try { 118 * throw new RuntimeException(); 119 * } catch (RuntimeException e) { 120 * // This is expected 121 * } 122 * ... 123 * try { 124 * throw new RuntimeException(); 125 * } catch (RuntimeException e) { 126 * // This is expected 127 * // some another comment 128 * } 129 * ... 130 * try { 131 * throw new RuntimeException(); 132 * } catch (RuntimeException e) { 133 * /* This is expected */ 134 * } 135 * ... 136 * try { 137 * throw new RuntimeException(); 138 * } catch (RuntimeException e) { 139 * /* 140 * * 141 * * This is expected 142 * * some another comment 143 * */ 144 * } 145 * ... 146 * try { 147 * throw new RuntimeException(); 148 * } catch (RuntimeException myException) { 149 * 150 * } 151 * </pre> 152 * <p> 153 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 154 * </p> 155 * <p> 156 * Violation Message Keys: 157 * </p> 158 * <ul> 159 * <li> 160 * {@code catch.block.empty} 161 * </li> 162 * </ul> 163 * 164 * @since 6.4 165 */ 166@StatelessCheck 167public class EmptyCatchBlockCheck extends AbstractCheck { 168 169 /** 170 * A key is pointing to the warning message text in "messages.properties" 171 * file. 172 */ 173 public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty"; 174 175 /** 176 * A pattern to split on line ends. 177 */ 178 private static final Pattern LINE_END_PATTERN = Pattern.compile("\\r?+\\n|\\r"); 179 180 /** 181 * 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 private Pattern exceptionVariableName = Pattern.compile("^$"); 185 186 /** 187 * Specify the RegExp for the first comment inside empty catch block. 188 * If check meets comment inside empty catch block matching specified format - empty 189 * block is suppressed. If it is multi-line comment - only its first line is analyzed. 190 */ 191 private Pattern commentFormat = Pattern.compile(".*"); 192 193 /** 194 * Setter to specify the RegExp for the name of the variable associated with exception. 195 * If check meets variable name matching specified value - empty block is suppressed. 196 * 197 * @param exceptionVariablePattern 198 * pattern of exception's variable name. 199 */ 200 public void setExceptionVariableName(Pattern exceptionVariablePattern) { 201 exceptionVariableName = exceptionVariablePattern; 202 } 203 204 /** 205 * Setter to specify the RegExp for the first comment inside empty catch block. 206 * If check meets comment inside empty catch block matching specified format - empty 207 * block is suppressed. If it is multi-line comment - only its first line is analyzed. 208 * 209 * @param commentPattern 210 * pattern of comment. 211 */ 212 public void setCommentFormat(Pattern commentPattern) { 213 commentFormat = commentPattern; 214 } 215 216 @Override 217 public int[] getDefaultTokens() { 218 return getRequiredTokens(); 219 } 220 221 @Override 222 public int[] getAcceptableTokens() { 223 return getRequiredTokens(); 224 } 225 226 @Override 227 public int[] getRequiredTokens() { 228 return new int[] { 229 TokenTypes.LITERAL_CATCH, 230 }; 231 } 232 233 @Override 234 public boolean isCommentNodesRequired() { 235 return true; 236 } 237 238 @Override 239 public void visitToken(DetailAST ast) { 240 visitCatchBlock(ast); 241 } 242 243 /** 244 * Visits catch ast node, if it is empty catch block - checks it according to 245 * Check's options. If exception's variable name or comment inside block are matching 246 * specified regexp - skips from consideration, else - puts violation. 247 * 248 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 249 */ 250 private void visitCatchBlock(DetailAST catchAst) { 251 if (isEmptyCatchBlock(catchAst)) { 252 final String commentContent = getCommentFirstLine(catchAst); 253 if (isVerifiable(catchAst, commentContent)) { 254 log(catchAst.findFirstToken(TokenTypes.SLIST), MSG_KEY_CATCH_BLOCK_EMPTY); 255 } 256 } 257 } 258 259 /** 260 * Gets the first line of comment in catch block. If comment is single-line - 261 * returns it fully, else if comment is multi-line - returns the first line. 262 * 263 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 264 * @return the first line of comment in catch block, "" if no comment was found. 265 */ 266 private static String getCommentFirstLine(DetailAST catchAst) { 267 final DetailAST slistToken = catchAst.getLastChild(); 268 final DetailAST firstElementInBlock = slistToken.getFirstChild(); 269 String commentContent = ""; 270 if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 271 commentContent = firstElementInBlock.getFirstChild().getText(); 272 } 273 else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 274 commentContent = firstElementInBlock.getFirstChild().getText(); 275 final String[] lines = LINE_END_PATTERN.split(commentContent); 276 for (String line : lines) { 277 if (!line.isEmpty()) { 278 commentContent = line; 279 break; 280 } 281 } 282 } 283 return commentContent; 284 } 285 286 /** 287 * Checks if current empty catch block is verifiable according to Check's options 288 * (exception's variable name and comment format are both in consideration). 289 * 290 * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block. 291 * @param commentContent text of comment. 292 * @return true if empty catch block is verifiable by Check. 293 */ 294 private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) { 295 final String variableName = getExceptionVariableName(emptyCatchAst); 296 final boolean isMatchingVariableName = exceptionVariableName 297 .matcher(variableName).find(); 298 final boolean isMatchingCommentContent = !commentContent.isEmpty() 299 && commentFormat.matcher(commentContent).find(); 300 return !isMatchingVariableName && !isMatchingCommentContent; 301 } 302 303 /** 304 * Checks if catch block is empty or contains only comments. 305 * 306 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 307 * @return true if catch block is empty. 308 */ 309 private static boolean isEmptyCatchBlock(DetailAST catchAst) { 310 boolean result = true; 311 final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST); 312 DetailAST catchBlockStmt = slistToken.getFirstChild(); 313 while (catchBlockStmt.getType() != TokenTypes.RCURLY) { 314 if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT 315 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) { 316 result = false; 317 break; 318 } 319 catchBlockStmt = catchBlockStmt.getNextSibling(); 320 } 321 return result; 322 } 323 324 /** 325 * Gets variable's name associated with exception. 326 * 327 * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} 328 * @return Variable's name associated with exception. 329 */ 330 private static String getExceptionVariableName(DetailAST catchAst) { 331 final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF); 332 final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT); 333 return variableName.getText(); 334 } 335 336}