001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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.Optional; 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; 029import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 030 031/** 032 * <p> 033 * Checks for braces around code blocks. 034 * </p> 035 * <ul> 036 * <li> 037 * Property {@code allowEmptyLoopBody} - Allow loops with empty bodies. 038 * Type is {@code boolean}. 039 * Default value is {@code false}. 040 * </li> 041 * <li> 042 * Property {@code allowSingleLineStatement} - Allow single-line statements without braces. 043 * Type is {@code boolean}. 044 * Default value is {@code false}. 045 * </li> 046 * <li> 047 * Property {@code tokens} - tokens to check 048 * Type is {@code java.lang.String[]}. 049 * Validation type is {@code tokenSet}. 050 * Default value is: 051 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO"> 052 * LITERAL_DO</a>, 053 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE"> 054 * LITERAL_ELSE</a>, 055 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR"> 056 * LITERAL_FOR</a>, 057 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF"> 058 * LITERAL_IF</a>, 059 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE"> 060 * LITERAL_WHILE</a>. 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 needBraces} 072 * </li> 073 * </ul> 074 * 075 * @since 3.0 076 */ 077@StatelessCheck 078public class NeedBracesCheck extends AbstractCheck { 079 080 /** 081 * A key is pointing to the warning message text in "messages.properties" 082 * file. 083 */ 084 public static final String MSG_KEY_NEED_BRACES = "needBraces"; 085 086 /** 087 * Allow single-line statements without braces. 088 */ 089 private boolean allowSingleLineStatement; 090 091 /** 092 * Allow loops with empty bodies. 093 */ 094 private boolean allowEmptyLoopBody; 095 096 /** 097 * Setter to allow single-line statements without braces. 098 * 099 * @param allowSingleLineStatement Check's option for skipping single-line statements 100 * @since 6.5 101 */ 102 public void setAllowSingleLineStatement(boolean allowSingleLineStatement) { 103 this.allowSingleLineStatement = allowSingleLineStatement; 104 } 105 106 /** 107 * Setter to allow loops with empty bodies. 108 * 109 * @param allowEmptyLoopBody Check's option for allowing loops with empty body. 110 * @since 6.12.1 111 */ 112 public void setAllowEmptyLoopBody(boolean allowEmptyLoopBody) { 113 this.allowEmptyLoopBody = allowEmptyLoopBody; 114 } 115 116 @Override 117 public int[] getDefaultTokens() { 118 return new int[] { 119 TokenTypes.LITERAL_DO, 120 TokenTypes.LITERAL_ELSE, 121 TokenTypes.LITERAL_FOR, 122 TokenTypes.LITERAL_IF, 123 TokenTypes.LITERAL_WHILE, 124 }; 125 } 126 127 @Override 128 public int[] getAcceptableTokens() { 129 return new int[] { 130 TokenTypes.LITERAL_DO, 131 TokenTypes.LITERAL_ELSE, 132 TokenTypes.LITERAL_FOR, 133 TokenTypes.LITERAL_IF, 134 TokenTypes.LITERAL_WHILE, 135 TokenTypes.LITERAL_CASE, 136 TokenTypes.LITERAL_DEFAULT, 137 TokenTypes.LAMBDA, 138 }; 139 } 140 141 @Override 142 public int[] getRequiredTokens() { 143 return CommonUtil.EMPTY_INT_ARRAY; 144 } 145 146 @Override 147 public void visitToken(DetailAST ast) { 148 final boolean hasNoSlist = ast.findFirstToken(TokenTypes.SLIST) == null; 149 if (hasNoSlist && !isSkipStatement(ast) && isBracesNeeded(ast)) { 150 log(ast, MSG_KEY_NEED_BRACES, ast.getText()); 151 } 152 } 153 154 /** 155 * Checks if token needs braces. 156 * Some tokens have additional conditions: 157 * <ul> 158 * <li>{@link TokenTypes#LITERAL_FOR}</li> 159 * <li>{@link TokenTypes#LITERAL_WHILE}</li> 160 * <li>{@link TokenTypes#LITERAL_CASE}</li> 161 * <li>{@link TokenTypes#LITERAL_DEFAULT}</li> 162 * <li>{@link TokenTypes#LITERAL_ELSE}</li> 163 * <li>{@link TokenTypes#LAMBDA}</li> 164 * </ul> 165 * For all others default value {@code true} is returned. 166 * 167 * @param ast token to check 168 * @return result of additional checks for specific token types, 169 * {@code true} if there is no additional checks for token 170 */ 171 private boolean isBracesNeeded(DetailAST ast) { 172 final boolean result; 173 switch (ast.getType()) { 174 case TokenTypes.LITERAL_FOR: 175 case TokenTypes.LITERAL_WHILE: 176 result = !isEmptyLoopBodyAllowed(ast); 177 break; 178 case TokenTypes.LITERAL_CASE: 179 case TokenTypes.LITERAL_DEFAULT: 180 result = hasUnbracedStatements(ast) 181 && !isSwitchLabeledExpression(ast); 182 break; 183 case TokenTypes.LITERAL_ELSE: 184 result = ast.findFirstToken(TokenTypes.LITERAL_IF) == null; 185 break; 186 case TokenTypes.LAMBDA: 187 result = !isInSwitchRule(ast); 188 break; 189 default: 190 result = true; 191 break; 192 } 193 return result; 194 } 195 196 /** 197 * Checks if current loop has empty body and can be skipped by this check. 198 * 199 * @param ast for, while statements. 200 * @return true if current loop can be skipped by check. 201 */ 202 private boolean isEmptyLoopBodyAllowed(DetailAST ast) { 203 return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null; 204 } 205 206 /** 207 * Checks if switch member (case, default statements) has statements without curly braces. 208 * 209 * @param ast case, default statements. 210 * @return true if switch member has unbraced statements, false otherwise. 211 */ 212 private static boolean hasUnbracedStatements(DetailAST ast) { 213 final DetailAST nextSibling = ast.getNextSibling(); 214 boolean result = false; 215 216 if (isInSwitchRule(ast)) { 217 final DetailAST parent = ast.getParent(); 218 result = parent.getLastChild().getType() != TokenTypes.SLIST; 219 } 220 else if (nextSibling != null 221 && nextSibling.getType() == TokenTypes.SLIST 222 && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) { 223 result = true; 224 } 225 return result; 226 } 227 228 /** 229 * Checks if current statement can be skipped by "need braces" warning. 230 * 231 * @param statement if, for, while, do-while, lambda, else, case, default statements. 232 * @return true if current statement can be skipped by Check. 233 */ 234 private boolean isSkipStatement(DetailAST statement) { 235 return allowSingleLineStatement && isSingleLineStatement(statement); 236 } 237 238 /** 239 * Checks if current statement is single-line statement, e.g.: 240 * <p> 241 * {@code 242 * if (obj.isValid()) return true; 243 * } 244 * </p> 245 * <p> 246 * {@code 247 * while (obj.isValid()) return true; 248 * } 249 * </p> 250 * 251 * @param statement if, for, while, do-while, lambda, else, case, default statements. 252 * @return true if current statement is single-line statement. 253 */ 254 private static boolean isSingleLineStatement(DetailAST statement) { 255 final boolean result; 256 257 switch (statement.getType()) { 258 case TokenTypes.LITERAL_IF: 259 result = isSingleLineIf(statement); 260 break; 261 case TokenTypes.LITERAL_FOR: 262 result = isSingleLineFor(statement); 263 break; 264 case TokenTypes.LITERAL_DO: 265 result = isSingleLineDoWhile(statement); 266 break; 267 case TokenTypes.LITERAL_WHILE: 268 result = isSingleLineWhile(statement); 269 break; 270 case TokenTypes.LAMBDA: 271 result = !isInSwitchRule(statement) 272 && isSingleLineLambda(statement); 273 break; 274 case TokenTypes.LITERAL_CASE: 275 case TokenTypes.LITERAL_DEFAULT: 276 result = isSingleLineSwitchMember(statement); 277 break; 278 default: 279 result = isSingleLineElse(statement); 280 break; 281 } 282 283 return result; 284 } 285 286 /** 287 * Checks if current while statement is single-line statement, e.g.: 288 * <p> 289 * {@code 290 * while (obj.isValid()) return true; 291 * } 292 * </p> 293 * 294 * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}. 295 * @return true if current while statement is single-line statement. 296 */ 297 private static boolean isSingleLineWhile(DetailAST literalWhile) { 298 boolean result = false; 299 if (literalWhile.getParent().getType() == TokenTypes.SLIST) { 300 final DetailAST block = literalWhile.getLastChild().getPreviousSibling(); 301 result = TokenUtil.areOnSameLine(literalWhile, block); 302 } 303 return result; 304 } 305 306 /** 307 * Checks if current do-while statement is single-line statement, e.g.: 308 * <p> 309 * {@code 310 * do this.notify(); while (o != null); 311 * } 312 * </p> 313 * 314 * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}. 315 * @return true if current do-while statement is single-line statement. 316 */ 317 private static boolean isSingleLineDoWhile(DetailAST literalDo) { 318 boolean result = false; 319 if (literalDo.getParent().getType() == TokenTypes.SLIST) { 320 final DetailAST block = literalDo.getFirstChild(); 321 result = TokenUtil.areOnSameLine(block, literalDo); 322 } 323 return result; 324 } 325 326 /** 327 * Checks if current for statement is single-line statement, e.g.: 328 * <p> 329 * {@code 330 * for (int i = 0; ; ) this.notify(); 331 * } 332 * </p> 333 * 334 * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}. 335 * @return true if current for statement is single-line statement. 336 */ 337 private static boolean isSingleLineFor(DetailAST literalFor) { 338 boolean result = false; 339 if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) { 340 result = true; 341 } 342 else if (literalFor.getParent().getType() == TokenTypes.SLIST) { 343 result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild()); 344 } 345 return result; 346 } 347 348 /** 349 * Checks if current if statement is single-line statement, e.g.: 350 * <p> 351 * {@code 352 * if (obj.isValid()) return true; 353 * } 354 * </p> 355 * 356 * @param literalIf {@link TokenTypes#LITERAL_IF if statement}. 357 * @return true if current if statement is single-line statement. 358 */ 359 private static boolean isSingleLineIf(DetailAST literalIf) { 360 boolean result = false; 361 if (literalIf.getParent().getType() == TokenTypes.SLIST) { 362 final DetailAST literalIfLastChild = literalIf.getLastChild(); 363 final DetailAST block; 364 if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) { 365 block = literalIfLastChild.getPreviousSibling(); 366 } 367 else { 368 block = literalIfLastChild; 369 } 370 final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR); 371 result = TokenUtil.areOnSameLine(ifCondition, block); 372 } 373 return result; 374 } 375 376 /** 377 * Checks if current lambda statement is single-line statement, e.g.: 378 * <p> 379 * {@code 380 * Runnable r = () -> System.out.println("Hello, world!"); 381 * } 382 * </p> 383 * 384 * @param lambda {@link TokenTypes#LAMBDA lambda statement}. 385 * @return true if current lambda statement is single-line statement. 386 */ 387 private static boolean isSingleLineLambda(DetailAST lambda) { 388 final DetailAST lastLambdaToken = getLastLambdaToken(lambda); 389 return TokenUtil.areOnSameLine(lambda, lastLambdaToken); 390 } 391 392 /** 393 * Looks for the last token in lambda. 394 * 395 * @param lambda token to check. 396 * @return last token in lambda 397 */ 398 private static DetailAST getLastLambdaToken(DetailAST lambda) { 399 DetailAST node = lambda; 400 do { 401 node = node.getLastChild(); 402 } while (node.getLastChild() != null); 403 return node; 404 } 405 406 /** 407 * Checks if current ast's parent is a switch rule, e.g.: 408 * <p> 409 * {@code 410 * case 1 -> monthString = "January"; 411 * } 412 * </p> 413 * 414 * @param ast the ast to check. 415 * @return true if current ast belongs to a switch rule. 416 */ 417 private static boolean isInSwitchRule(DetailAST ast) { 418 return ast.getParent().getType() == TokenTypes.SWITCH_RULE; 419 } 420 421 /** 422 * Checks if current expression is a switch labeled expression. If so, 423 * braces are not allowed e.g.: 424 * <p> 425 * {@code 426 * case 1 -> 4; 427 * } 428 * </p> 429 * 430 * @param ast the ast to check 431 * @return true if current expression is a switch labeled expression. 432 */ 433 private static boolean isSwitchLabeledExpression(DetailAST ast) { 434 final DetailAST parent = ast.getParent(); 435 return switchRuleHasSingleExpression(parent); 436 } 437 438 /** 439 * Checks if current switch labeled expression contains only a single expression. 440 * 441 * @param switchRule {@link TokenTypes#SWITCH_RULE}. 442 * @return true if current switch rule has a single expression. 443 */ 444 private static boolean switchRuleHasSingleExpression(DetailAST switchRule) { 445 final DetailAST possibleExpression = switchRule.findFirstToken(TokenTypes.EXPR); 446 return possibleExpression != null 447 && possibleExpression.getFirstChild().getFirstChild() == null; 448 } 449 450 /** 451 * Checks if switch member (case or default statement) in a switch rule or 452 * case group is on a single-line. 453 * 454 * @param statement {@link TokenTypes#LITERAL_CASE case statement} or 455 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 456 * @return true if current switch member is single-line statement. 457 */ 458 private static boolean isSingleLineSwitchMember(DetailAST statement) { 459 final boolean result; 460 if (isInSwitchRule(statement)) { 461 result = isSingleLineSwitchRule(statement); 462 } 463 else { 464 result = isSingleLineCaseGroup(statement); 465 } 466 return result; 467 } 468 469 /** 470 * Checks if switch member in case group (case or default statement) 471 * is single-line statement, e.g.: 472 * <p> 473 * {@code 474 * case 1: System.out.println("case one"); break; 475 * case 2: System.out.println("case two"); break; 476 * case 3: ; 477 * default: System.out.println("default"); break; 478 * } 479 * </p> 480 * 481 * 482 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or 483 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 484 * @return true if current switch member is single-line statement. 485 */ 486 private static boolean isSingleLineCaseGroup(DetailAST ast) { 487 return Optional.of(ast) 488 .map(DetailAST::getNextSibling) 489 .map(DetailAST::getLastChild) 490 .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken)) 491 .orElse(Boolean.TRUE); 492 } 493 494 /** 495 * Checks if switch member in switch rule (case or default statement) is 496 * single-line statement, e.g.: 497 * <p> 498 * {@code 499 * case 1 -> System.out.println("case one"); 500 * case 2 -> System.out.println("case two"); 501 * default -> System.out.println("default"); 502 * } 503 * </p> 504 * 505 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or 506 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 507 * @return true if current switch label is single-line statement. 508 */ 509 private static boolean isSingleLineSwitchRule(DetailAST ast) { 510 final DetailAST lastSibling = ast.getParent().getLastChild(); 511 return TokenUtil.areOnSameLine(ast, lastSibling); 512 } 513 514 /** 515 * Checks if current else statement is single-line statement, e.g.: 516 * <p> 517 * {@code 518 * else doSomeStuff(); 519 * } 520 * </p> 521 * 522 * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}. 523 * @return true if current else statement is single-line statement. 524 */ 525 private static boolean isSingleLineElse(DetailAST literalElse) { 526 final DetailAST block = literalElse.getFirstChild(); 527 return TokenUtil.areOnSameLine(literalElse, block); 528 } 529 530}