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