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