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