001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.coding; 021 022import java.util.Optional; 023import java.util.regex.Pattern; 024 025import com.puppycrawl.tools.checkstyle.StatelessCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 030 031/** 032 * <p> 033 * Checks for fall-through in {@code switch} statements. 034 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a 035 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement. 036 * </p> 037 * <p> 038 * The check honors special comments to suppress the warning. 039 * By default, the texts 040 * "fallthru", "fall thru", "fall-thru", 041 * "fallthrough", "fall through", "fall-through" 042 * "fallsthrough", "falls through", "falls-through" (case-sensitive). 043 * The comment containing these words must be all on one line, 044 * and must be on the last non-empty line before the {@code case} triggering 045 * the warning or on the same line before the {@code case}(ugly, but possible). 046 * </p> 047 * <p> 048 * Note: The check assumes that there is no unreachable code in the {@code case}. 049 * </p> 050 * <ul> 051 * <li> 052 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked. 053 * Type is {@code boolean}. 054 * Default value is {@code false}. 055 * </li> 056 * <li> 057 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses 058 * the warning about a fall through. 059 * Type is {@code java.util.regex.Pattern}. 060 * Default value is {@code "falls?[ -]?thr(u|ough)"}. 061 * </li> 062 * </ul> 063 * <p> 064 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 065 * </p> 066 * <p> 067 * Violation Message Keys: 068 * </p> 069 * <ul> 070 * <li> 071 * {@code fall.through} 072 * </li> 073 * <li> 074 * {@code fall.through.last} 075 * </li> 076 * </ul> 077 * 078 * @since 3.4 079 */ 080@StatelessCheck 081public class FallThroughCheck extends AbstractCheck { 082 083 /** 084 * A key is pointing to the warning message text in "messages.properties" 085 * file. 086 */ 087 public static final String MSG_FALL_THROUGH = "fall.through"; 088 089 /** 090 * A key is pointing to the warning message text in "messages.properties" 091 * file. 092 */ 093 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 094 095 /** Control whether the last case group must be checked. */ 096 private boolean checkLastCaseGroup; 097 098 /** 099 * Define the RegExp to match the relief comment that suppresses 100 * the warning about a fall through. 101 */ 102 private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)"); 103 104 @Override 105 public int[] getDefaultTokens() { 106 return getRequiredTokens(); 107 } 108 109 @Override 110 public int[] getRequiredTokens() { 111 return new int[] {TokenTypes.CASE_GROUP}; 112 } 113 114 @Override 115 public int[] getAcceptableTokens() { 116 return getRequiredTokens(); 117 } 118 119 @Override 120 public boolean isCommentNodesRequired() { 121 return true; 122 } 123 124 /** 125 * Setter to define the RegExp to match the relief comment that suppresses 126 * the warning about a fall through. 127 * 128 * @param pattern 129 * The regular expression pattern. 130 * @since 4.0 131 */ 132 public void setReliefPattern(Pattern pattern) { 133 reliefPattern = pattern; 134 } 135 136 /** 137 * Setter to control whether the last case group must be checked. 138 * 139 * @param value new value of the property. 140 * @since 4.0 141 */ 142 public void setCheckLastCaseGroup(boolean value) { 143 checkLastCaseGroup = value; 144 } 145 146 @Override 147 public void visitToken(DetailAST ast) { 148 final DetailAST nextGroup = ast.getNextSibling(); 149 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 150 if (!isLastGroup || checkLastCaseGroup) { 151 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 152 153 if (slist != null && !isTerminated(slist, true, true) 154 && !hasFallThroughComment(ast)) { 155 if (isLastGroup) { 156 log(ast, MSG_FALL_THROUGH_LAST); 157 } 158 else { 159 log(nextGroup, MSG_FALL_THROUGH); 160 } 161 } 162 } 163 } 164 165 /** 166 * Checks if a given subtree terminated by return, throw or, 167 * if allowed break, continue. 168 * 169 * @param ast root of given subtree 170 * @param useBreak should we consider break as terminator 171 * @param useContinue should we consider continue as terminator 172 * @return true if the subtree is terminated. 173 */ 174 private boolean isTerminated(final DetailAST ast, boolean useBreak, 175 boolean useContinue) { 176 final boolean terminated; 177 178 switch (ast.getType()) { 179 case TokenTypes.LITERAL_RETURN: 180 case TokenTypes.LITERAL_YIELD: 181 case TokenTypes.LITERAL_THROW: 182 terminated = true; 183 break; 184 case TokenTypes.LITERAL_BREAK: 185 terminated = useBreak; 186 break; 187 case TokenTypes.LITERAL_CONTINUE: 188 terminated = useContinue; 189 break; 190 case TokenTypes.SLIST: 191 terminated = checkSlist(ast, useBreak, useContinue); 192 break; 193 case TokenTypes.LITERAL_IF: 194 terminated = checkIf(ast, useBreak, useContinue); 195 break; 196 case TokenTypes.LITERAL_FOR: 197 case TokenTypes.LITERAL_WHILE: 198 case TokenTypes.LITERAL_DO: 199 terminated = checkLoop(ast); 200 break; 201 case TokenTypes.LITERAL_TRY: 202 terminated = checkTry(ast, useBreak, useContinue); 203 break; 204 case TokenTypes.LITERAL_SWITCH: 205 terminated = checkSwitch(ast, useContinue); 206 break; 207 case TokenTypes.LITERAL_SYNCHRONIZED: 208 terminated = checkSynchronized(ast, useBreak, useContinue); 209 break; 210 default: 211 terminated = false; 212 } 213 return terminated; 214 } 215 216 /** 217 * Checks if a given SLIST terminated by return, throw or, 218 * if allowed break, continue. 219 * 220 * @param slistAst SLIST to check 221 * @param useBreak should we consider break as terminator 222 * @param useContinue should we consider continue as terminator 223 * @return true if SLIST is terminated. 224 */ 225 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 226 boolean useContinue) { 227 DetailAST lastStmt = slistAst.getLastChild(); 228 229 if (lastStmt.getType() == TokenTypes.RCURLY) { 230 lastStmt = lastStmt.getPreviousSibling(); 231 } 232 233 while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT, 234 TokenTypes.BLOCK_COMMENT_BEGIN)) { 235 lastStmt = lastStmt.getPreviousSibling(); 236 } 237 238 return lastStmt != null 239 && isTerminated(lastStmt, useBreak, useContinue); 240 } 241 242 /** 243 * Checks if a given IF terminated by return, throw or, 244 * if allowed break, continue. 245 * 246 * @param ast IF to check 247 * @param useBreak should we consider break as terminator 248 * @param useContinue should we consider continue as terminator 249 * @return true if IF is terminated. 250 */ 251 private boolean checkIf(final DetailAST ast, boolean useBreak, 252 boolean useContinue) { 253 final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN)); 254 255 final DetailAST elseStmt = getNextNonCommentAst(thenStmt); 256 257 return elseStmt != null 258 && isTerminated(thenStmt, useBreak, useContinue) 259 && isTerminated(elseStmt.getLastChild(), useBreak, useContinue); 260 } 261 262 /** 263 * This method will skip the comment content while finding the next ast of current ast. 264 * 265 * @param ast current ast 266 * @return next ast after skipping comment 267 */ 268 private static DetailAST getNextNonCommentAst(DetailAST ast) { 269 DetailAST nextSibling = ast.getNextSibling(); 270 while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT, 271 TokenTypes.BLOCK_COMMENT_BEGIN)) { 272 nextSibling = nextSibling.getNextSibling(); 273 } 274 return nextSibling; 275 } 276 277 /** 278 * Checks if a given loop terminated by return, throw or, 279 * if allowed break, continue. 280 * 281 * @param ast loop to check 282 * @return true if loop is terminated. 283 */ 284 private boolean checkLoop(final DetailAST ast) { 285 final DetailAST loopBody; 286 if (ast.getType() == TokenTypes.LITERAL_DO) { 287 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 288 loopBody = lparen.getPreviousSibling(); 289 } 290 else { 291 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 292 loopBody = rparen.getNextSibling(); 293 } 294 return isTerminated(loopBody, false, false); 295 } 296 297 /** 298 * Checks if a given try/catch/finally block terminated by return, throw or, 299 * if allowed break, continue. 300 * 301 * @param ast loop to check 302 * @param useBreak should we consider break as terminator 303 * @param useContinue should we consider continue as terminator 304 * @return true if try/catch/finally block is terminated 305 */ 306 private boolean checkTry(final DetailAST ast, boolean useBreak, 307 boolean useContinue) { 308 final DetailAST finalStmt = ast.getLastChild(); 309 boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY 310 && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 311 312 if (!isTerminated) { 313 DetailAST firstChild = ast.getFirstChild(); 314 315 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 316 firstChild = firstChild.getNextSibling(); 317 } 318 319 isTerminated = isTerminated(firstChild, 320 useBreak, useContinue); 321 322 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 323 while (catchStmt != null 324 && isTerminated 325 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 326 final DetailAST catchBody = 327 catchStmt.findFirstToken(TokenTypes.SLIST); 328 isTerminated = isTerminated(catchBody, useBreak, useContinue); 329 catchStmt = catchStmt.getNextSibling(); 330 } 331 } 332 return isTerminated; 333 } 334 335 /** 336 * Checks if a given switch terminated by return, throw or, 337 * if allowed break, continue. 338 * 339 * @param literalSwitchAst loop to check 340 * @param useContinue should we consider continue as terminator 341 * @return true if switch is terminated 342 */ 343 private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) { 344 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 345 boolean isTerminated = caseGroup != null; 346 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 347 final DetailAST caseBody = 348 caseGroup.findFirstToken(TokenTypes.SLIST); 349 isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue); 350 caseGroup = caseGroup.getNextSibling(); 351 } 352 return isTerminated; 353 } 354 355 /** 356 * Checks if a given synchronized block terminated by return, throw or, 357 * if allowed break, continue. 358 * 359 * @param synchronizedAst synchronized block to check. 360 * @param useBreak should we consider break as terminator 361 * @param useContinue should we consider continue as terminator 362 * @return true if synchronized block is terminated 363 */ 364 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 365 boolean useContinue) { 366 return isTerminated( 367 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 368 } 369 370 /** 371 * Determines if the fall through case between {@code currentCase} and 372 * {@code nextCase} is relieved by an appropriate comment. 373 * 374 * <p>Handles</p> 375 * <pre> 376 * case 1: 377 * /* FALLTHRU */ case 2: 378 * 379 * switch(i) { 380 * default: 381 * /* FALLTHRU */} 382 * 383 * case 1: 384 * // FALLTHRU 385 * case 2: 386 * 387 * switch(i) { 388 * default: 389 * // FALLTHRU 390 * </pre> 391 * 392 * @param currentCase AST of the case that falls through to the next case. 393 * @return True if a relief comment was found 394 */ 395 private boolean hasFallThroughComment(DetailAST currentCase) { 396 final DetailAST nextSibling = currentCase.getNextSibling(); 397 final DetailAST ast; 398 if (nextSibling.getType() == TokenTypes.CASE_GROUP) { 399 ast = nextSibling.getFirstChild(); 400 } 401 else { 402 ast = currentCase; 403 } 404 return hasReliefComment(ast); 405 } 406 407 /** 408 * Check if there is any fall through comment. 409 * 410 * @param ast ast to check 411 * @return true if relief comment found 412 */ 413 private boolean hasReliefComment(DetailAST ast) { 414 return Optional.ofNullable(getNextNonCommentAst(ast)) 415 .map(DetailAST::getPreviousSibling) 416 .map(previous -> previous.getFirstChild().getText()) 417 .map(text -> reliefPattern.matcher(text).find()) 418 .orElse(Boolean.FALSE); 419 } 420 421}