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