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 break; 182 case TokenTypes.LITERAL_ELSE: 183 result = ast.findFirstToken(TokenTypes.LITERAL_IF) == null; 184 break; 185 case TokenTypes.LAMBDA: 186 result = !isInSwitchRule(ast); 187 break; 188 default: 189 result = true; 190 break; 191 } 192 return result; 193 } 194 195 /** 196 * Checks if current loop has empty body and can be skipped by this check. 197 * 198 * @param ast for, while statements. 199 * @return true if current loop can be skipped by check. 200 */ 201 private boolean isEmptyLoopBodyAllowed(DetailAST ast) { 202 return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null; 203 } 204 205 /** 206 * Checks if switch member (case, default statements) has statements without curly braces. 207 * 208 * @param ast case, default statements. 209 * @return true if switch member has unbraced statements, false otherwise. 210 */ 211 private static boolean hasUnbracedStatements(DetailAST ast) { 212 final DetailAST nextSibling = ast.getNextSibling(); 213 boolean result = false; 214 215 if (isInSwitchRule(ast)) { 216 final DetailAST parent = ast.getParent(); 217 result = parent.getLastChild().getType() != TokenTypes.SLIST; 218 } 219 else if (nextSibling != null 220 && nextSibling.getType() == TokenTypes.SLIST 221 && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) { 222 result = true; 223 } 224 return result; 225 } 226 227 /** 228 * Checks if current statement can be skipped by "need braces" warning. 229 * 230 * @param statement if, for, while, do-while, lambda, else, case, default statements. 231 * @return true if current statement can be skipped by Check. 232 */ 233 private boolean isSkipStatement(DetailAST statement) { 234 return allowSingleLineStatement && isSingleLineStatement(statement); 235 } 236 237 /** 238 * Checks if current statement is single-line statement, e.g.: 239 * <p> 240 * {@code 241 * if (obj.isValid()) return true; 242 * } 243 * </p> 244 * <p> 245 * {@code 246 * while (obj.isValid()) return true; 247 * } 248 * </p> 249 * 250 * @param statement if, for, while, do-while, lambda, else, case, default statements. 251 * @return true if current statement is single-line statement. 252 */ 253 private static boolean isSingleLineStatement(DetailAST statement) { 254 final boolean result; 255 256 switch (statement.getType()) { 257 case TokenTypes.LITERAL_IF: 258 result = isSingleLineIf(statement); 259 break; 260 case TokenTypes.LITERAL_FOR: 261 result = isSingleLineFor(statement); 262 break; 263 case TokenTypes.LITERAL_DO: 264 result = isSingleLineDoWhile(statement); 265 break; 266 case TokenTypes.LITERAL_WHILE: 267 result = isSingleLineWhile(statement); 268 break; 269 case TokenTypes.LAMBDA: 270 result = !isInSwitchRule(statement) 271 && isSingleLineLambda(statement); 272 break; 273 case TokenTypes.LITERAL_CASE: 274 case TokenTypes.LITERAL_DEFAULT: 275 result = isSingleLineSwitchMember(statement); 276 break; 277 default: 278 result = isSingleLineElse(statement); 279 break; 280 } 281 282 return result; 283 } 284 285 /** 286 * Checks if current while statement is single-line statement, e.g.: 287 * <p> 288 * {@code 289 * while (obj.isValid()) return true; 290 * } 291 * </p> 292 * 293 * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}. 294 * @return true if current while statement is single-line statement. 295 */ 296 private static boolean isSingleLineWhile(DetailAST literalWhile) { 297 boolean result = false; 298 if (literalWhile.getParent().getType() == TokenTypes.SLIST) { 299 final DetailAST block = literalWhile.getLastChild().getPreviousSibling(); 300 result = TokenUtil.areOnSameLine(literalWhile, block); 301 } 302 return result; 303 } 304 305 /** 306 * Checks if current do-while statement is single-line statement, e.g.: 307 * <p> 308 * {@code 309 * do this.notify(); while (o != null); 310 * } 311 * </p> 312 * 313 * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}. 314 * @return true if current do-while statement is single-line statement. 315 */ 316 private static boolean isSingleLineDoWhile(DetailAST literalDo) { 317 boolean result = false; 318 if (literalDo.getParent().getType() == TokenTypes.SLIST) { 319 final DetailAST block = literalDo.getFirstChild(); 320 result = TokenUtil.areOnSameLine(block, literalDo); 321 } 322 return result; 323 } 324 325 /** 326 * Checks if current for statement is single-line statement, e.g.: 327 * <p> 328 * {@code 329 * for (int i = 0; ; ) this.notify(); 330 * } 331 * </p> 332 * 333 * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}. 334 * @return true if current for statement is single-line statement. 335 */ 336 private static boolean isSingleLineFor(DetailAST literalFor) { 337 boolean result = false; 338 if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) { 339 result = true; 340 } 341 else if (literalFor.getParent().getType() == TokenTypes.SLIST) { 342 result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild()); 343 } 344 return result; 345 } 346 347 /** 348 * Checks if current if statement is single-line statement, e.g.: 349 * <p> 350 * {@code 351 * if (obj.isValid()) return true; 352 * } 353 * </p> 354 * 355 * @param literalIf {@link TokenTypes#LITERAL_IF if statement}. 356 * @return true if current if statement is single-line statement. 357 */ 358 private static boolean isSingleLineIf(DetailAST literalIf) { 359 boolean result = false; 360 if (literalIf.getParent().getType() == TokenTypes.SLIST) { 361 final DetailAST literalIfLastChild = literalIf.getLastChild(); 362 final DetailAST block; 363 if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) { 364 block = literalIfLastChild.getPreviousSibling(); 365 } 366 else { 367 block = literalIfLastChild; 368 } 369 final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR); 370 result = TokenUtil.areOnSameLine(ifCondition, block); 371 } 372 return result; 373 } 374 375 /** 376 * Checks if current lambda statement is single-line statement, e.g.: 377 * <p> 378 * {@code 379 * Runnable r = () -> System.out.println("Hello, world!"); 380 * } 381 * </p> 382 * 383 * @param lambda {@link TokenTypes#LAMBDA lambda statement}. 384 * @return true if current lambda statement is single-line statement. 385 */ 386 private static boolean isSingleLineLambda(DetailAST lambda) { 387 final DetailAST lastLambdaToken = getLastLambdaToken(lambda); 388 return TokenUtil.areOnSameLine(lambda, lastLambdaToken); 389 } 390 391 /** 392 * Looks for the last token in lambda. 393 * 394 * @param lambda token to check. 395 * @return last token in lambda 396 */ 397 private static DetailAST getLastLambdaToken(DetailAST lambda) { 398 DetailAST node = lambda; 399 do { 400 node = node.getLastChild(); 401 } while (node.getLastChild() != null); 402 return node; 403 } 404 405 /** 406 * Checks if current ast's parent is a switch rule, e.g.: 407 * <p> 408 * {@code 409 * case 1 -> monthString = "January"; 410 * } 411 * </p> 412 * 413 * @param ast the ast to check. 414 * @return true if current ast belongs to a switch rule. 415 */ 416 private static boolean isInSwitchRule(DetailAST ast) { 417 return ast.getParent().getType() == TokenTypes.SWITCH_RULE; 418 } 419 420 /** 421 * Checks if switch member (case or default statement) in a switch rule or 422 * case group is on a single-line. 423 * 424 * @param statement {@link TokenTypes#LITERAL_CASE case statement} or 425 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 426 * @return true if current switch member is single-line statement. 427 */ 428 private static boolean isSingleLineSwitchMember(DetailAST statement) { 429 final boolean result; 430 if (isInSwitchRule(statement)) { 431 result = isSingleLineSwitchRule(statement); 432 } 433 else { 434 result = isSingleLineCaseGroup(statement); 435 } 436 return result; 437 } 438 439 /** 440 * Checks if switch member in case group (case or default statement) 441 * is single-line statement, e.g.: 442 * <p> 443 * {@code 444 * case 1: System.out.println("case one"); break; 445 * case 2: System.out.println("case two"); break; 446 * case 3: ; 447 * default: System.out.println("default"); break; 448 * } 449 * </p> 450 * 451 * 452 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or 453 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 454 * @return true if current switch member is single-line statement. 455 */ 456 private static boolean isSingleLineCaseGroup(DetailAST ast) { 457 return Optional.of(ast) 458 .map(DetailAST::getNextSibling) 459 .map(DetailAST::getLastChild) 460 .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken)) 461 .orElse(Boolean.TRUE); 462 } 463 464 /** 465 * Checks if switch member in switch rule (case or default statement) is 466 * single-line statement, e.g.: 467 * <p> 468 * {@code 469 * case 1 -> System.out.println("case one"); 470 * case 2 -> System.out.println("case two"); 471 * default -> System.out.println("default"); 472 * } 473 * </p> 474 * 475 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or 476 * {@link TokenTypes#LITERAL_DEFAULT default statement}. 477 * @return true if current switch label is single-line statement. 478 */ 479 private static boolean isSingleLineSwitchRule(DetailAST ast) { 480 final DetailAST lastSibling = ast.getParent().getLastChild(); 481 return TokenUtil.areOnSameLine(ast, lastSibling); 482 } 483 484 /** 485 * Checks if current else statement is single-line statement, e.g.: 486 * <p> 487 * {@code 488 * else doSomeStuff(); 489 * } 490 * </p> 491 * 492 * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}. 493 * @return true if current else statement is single-line statement. 494 */ 495 private static boolean isSingleLineElse(DetailAST literalElse) { 496 final DetailAST block = literalElse.getFirstChild(); 497 return TokenUtil.areOnSameLine(literalElse, block); 498 } 499 500}