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.Arrays; 023import java.util.Locale; 024import java.util.Optional; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 031import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 032 033/** 034 * <div> 035 * Checks the placement of right curly braces (<code>'}'</code>) for code blocks. This check 036 * supports if-else, try-catch-finally blocks, switch statements, switch cases, switch default, 037 * while-loops, for-loops, method definitions, class definitions, constructor definitions, 038 * instance, static initialization blocks, annotation definitions and enum definitions. 039 * For right curly brace of expression blocks of arrays, lambdas and class instances 040 * please follow issue 041 * <a href="https://github.com/checkstyle/checkstyle/issues/5945">#5945</a>. 042 * For right curly brace of enum constant please follow issue 043 * <a href="https://github.com/checkstyle/checkstyle/issues/7519">#7519</a>. 044 * </div> 045 * 046 * @since 3.0 047 */ 048@StatelessCheck 049public class RightCurlyCheck extends AbstractCheck { 050 051 /** 052 * A key is pointing to the warning message text in "messages.properties" 053 * file. 054 */ 055 public static final String MSG_KEY_LINE_BREAK_BEFORE = "line.break.before"; 056 057 /** 058 * A key is pointing to the warning message text in "messages.properties" 059 * file. 060 */ 061 public static final String MSG_KEY_LINE_ALONE = "line.alone"; 062 063 /** 064 * A key is pointing to the warning message text in "messages.properties" 065 * file. 066 */ 067 public static final String MSG_KEY_LINE_SAME = "line.same"; 068 069 /** 070 * Specify the policy on placement of a right curly brace (<code>'}'</code>). 071 */ 072 private RightCurlyOption option = RightCurlyOption.SAME; 073 074 /** 075 * Creates a new {@code RightCurlyCheck} instance. 076 */ 077 public RightCurlyCheck() { 078 // no code by default 079 } 080 081 /** 082 * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>). 083 * 084 * @param optionStr string to decode option from 085 * @throws IllegalArgumentException if unable to decode 086 * @since 3.0 087 */ 088 public void setOption(String optionStr) { 089 option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); 090 } 091 092 @Override 093 public int[] getDefaultTokens() { 094 return new int[] { 095 TokenTypes.LITERAL_TRY, 096 TokenTypes.LITERAL_CATCH, 097 TokenTypes.LITERAL_FINALLY, 098 TokenTypes.LITERAL_IF, 099 TokenTypes.LITERAL_ELSE, 100 }; 101 } 102 103 @Override 104 public int[] getAcceptableTokens() { 105 return new int[] { 106 TokenTypes.LITERAL_TRY, 107 TokenTypes.LITERAL_CATCH, 108 TokenTypes.LITERAL_FINALLY, 109 TokenTypes.LITERAL_IF, 110 TokenTypes.LITERAL_ELSE, 111 TokenTypes.CLASS_DEF, 112 TokenTypes.METHOD_DEF, 113 TokenTypes.CTOR_DEF, 114 TokenTypes.LITERAL_FOR, 115 TokenTypes.LITERAL_WHILE, 116 TokenTypes.LITERAL_DO, 117 TokenTypes.STATIC_INIT, 118 TokenTypes.INSTANCE_INIT, 119 TokenTypes.ANNOTATION_DEF, 120 TokenTypes.ENUM_DEF, 121 TokenTypes.INTERFACE_DEF, 122 TokenTypes.RECORD_DEF, 123 TokenTypes.COMPACT_CTOR_DEF, 124 TokenTypes.LITERAL_SWITCH, 125 TokenTypes.LITERAL_CASE, 126 TokenTypes.LITERAL_DEFAULT, 127 }; 128 } 129 130 @Override 131 public int[] getRequiredTokens() { 132 return CommonUtil.EMPTY_INT_ARRAY; 133 } 134 135 @Override 136 public void visitToken(DetailAST ast) { 137 final Details details = Details.getDetails(ast); 138 final DetailAST rcurly = details.rcurly(); 139 140 if (rcurly != null) { 141 final String violation = validate(details); 142 if (!violation.isEmpty()) { 143 log(rcurly, violation, "}", rcurly.getColumnNo() + 1); 144 } 145 } 146 } 147 148 /** 149 * Does general validation. 150 * 151 * @param details for validation. 152 * @return violation message or empty string 153 * if there was no violation during validation. 154 */ 155 private String validate(Details details) { 156 String violation = ""; 157 if (shouldHaveLineBreakBefore(option, details)) { 158 violation = MSG_KEY_LINE_BREAK_BEFORE; 159 } 160 else if (shouldBeOnSameLine(option, details)) { 161 violation = MSG_KEY_LINE_SAME; 162 } 163 else if (shouldBeAloneOnLine(option, details, getLine(details.rcurly.getLineNo() - 1))) { 164 violation = MSG_KEY_LINE_ALONE; 165 } 166 return violation; 167 } 168 169 /** 170 * Checks whether a right curly should have a line break before. 171 * 172 * @param bracePolicy option for placing the right curly brace. 173 * @param details details for validation. 174 * @return true if a right curly should have a line break before. 175 */ 176 private static boolean shouldHaveLineBreakBefore(RightCurlyOption bracePolicy, 177 Details details) { 178 return bracePolicy == RightCurlyOption.SAME 179 && !hasLineBreakBefore(details.rcurly()) 180 && !TokenUtil.areOnSameLine(details.lcurly(), details.rcurly()); 181 } 182 183 /** 184 * Checks that a right curly should be on the same line as the next statement. 185 * 186 * @param bracePolicy option for placing the right curly brace 187 * @param details Details for validation 188 * @return true if a right curly should be alone on a line. 189 */ 190 private static boolean shouldBeOnSameLine(RightCurlyOption bracePolicy, Details details) { 191 return bracePolicy == RightCurlyOption.SAME 192 && !details.shouldCheckLastRcurly() 193 && !TokenUtil.areOnSameLine(details.rcurly(), details.nextToken()); 194 } 195 196 /** 197 * Checks that a right curly should be alone on a line. 198 * 199 * @param bracePolicy option for placing the right curly brace 200 * @param details Details for validation 201 * @param targetSrcLine A string with contents of rcurly's line 202 * @return true if a right curly should be alone on a line. 203 */ 204 private static boolean shouldBeAloneOnLine(RightCurlyOption bracePolicy, 205 Details details, 206 String targetSrcLine) { 207 return bracePolicy == RightCurlyOption.ALONE 208 && shouldBeAloneOnLineWithAloneOption(details, targetSrcLine) 209 || (bracePolicy == RightCurlyOption.ALONE_OR_SINGLELINE 210 || details.shouldCheckLastRcurly) 211 && shouldBeAloneOnLineWithNotAloneOption(details, targetSrcLine); 212 } 213 214 /** 215 * Whether right curly should be alone on line when ALONE option is used. 216 * 217 * @param details details for validation. 218 * @param targetSrcLine A string with contents of rcurly's line 219 * @return true, if right curly should be alone on line when ALONE option is used. 220 */ 221 private static boolean shouldBeAloneOnLineWithAloneOption(Details details, 222 String targetSrcLine) { 223 return !isAloneOnLine(details, targetSrcLine); 224 } 225 226 /** 227 * Whether right curly should be alone on line when ALONE_OR_SINGLELINE or SAME option is used. 228 * 229 * @param details details for validation. 230 * @param targetSrcLine A string with contents of rcurly's line 231 * @return true, if right curly should be alone on line 232 * when ALONE_OR_SINGLELINE or SAME option is used. 233 */ 234 private static boolean shouldBeAloneOnLineWithNotAloneOption(Details details, 235 String targetSrcLine) { 236 return shouldBeAloneOnLineWithAloneOption(details, targetSrcLine) 237 && !isBlockAloneOnSingleLine(details); 238 } 239 240 /** 241 * Checks whether right curly is alone on a line. 242 * 243 * @param details for validation. 244 * @param targetSrcLine A string with contents of rcurly's line 245 * @return true if right curly is alone on a line. 246 */ 247 private static boolean isAloneOnLine(Details details, String targetSrcLine) { 248 final DetailAST rcurly = details.rcurly(); 249 final DetailAST nextToken = details.nextToken(); 250 return (nextToken == null || !TokenUtil.areOnSameLine(rcurly, nextToken) 251 || skipDoubleBraceInstInit(details)) 252 && CommonUtil.hasWhitespaceBefore(details.rcurly().getColumnNo(), 253 targetSrcLine); 254 } 255 256 /** 257 * This method determines if the double brace initialization should be skipped over by the 258 * check. Double brace initializations are treated differently. The corresponding inner 259 * rcurly is treated as if it was alone on line even when it may be followed by another 260 * rcurly and a semi, raising no violations. 261 * <i>Please do note though that the line should not contain anything other than the following 262 * right curly and the semi following it or else violations will be raised.</i> 263 * Only the kind of double brace initializations shown in the following example code will be 264 * skipped over:<br> 265 * <pre> 266 * {@code Map<String, String> map = new LinkedHashMap<>() {{ 267 * put("alpha", "man"); 268 * }}; // no violation} 269 * </pre> 270 * 271 * @param details {@link Details} object containing the details relevant to the rcurly 272 * @return if the double brace initialization rcurly should be skipped over by the check 273 */ 274 private static boolean skipDoubleBraceInstInit(Details details) { 275 boolean skipDoubleBraceInstInit = false; 276 final DetailAST tokenAfterNextToken = Details.getNextToken(details.nextToken()); 277 if (tokenAfterNextToken != null) { 278 final DetailAST rcurly = details.rcurly(); 279 skipDoubleBraceInstInit = rcurly.getParent().getParent() 280 .getType() == TokenTypes.INSTANCE_INIT 281 && details.nextToken().getType() == TokenTypes.RCURLY 282 && !TokenUtil.areOnSameLine(rcurly, Details.getNextToken(tokenAfterNextToken)); 283 } 284 return skipDoubleBraceInstInit; 285 } 286 287 /** 288 * Checks whether block has a single-line format and is alone on a line. 289 * 290 * @param details for validation. 291 * @return true if block has single-line format and is alone on a line. 292 */ 293 private static boolean isBlockAloneOnSingleLine(Details details) { 294 DetailAST nextToken = details.nextToken(); 295 296 while (nextToken != null && nextToken.getType() == TokenTypes.LITERAL_ELSE) { 297 nextToken = Details.getNextToken(nextToken); 298 } 299 300 // sibling tokens should be allowed on a single line 301 final int[] tokensWithBlockSibling = { 302 TokenTypes.DO_WHILE, 303 TokenTypes.LITERAL_FINALLY, 304 TokenTypes.LITERAL_CATCH, 305 }; 306 307 if (TokenUtil.isOfType(nextToken, tokensWithBlockSibling)) { 308 final DetailAST parent = nextToken.getParent(); 309 nextToken = Details.getNextToken(parent); 310 } 311 312 return TokenUtil.areOnSameLine(details.lcurly(), details.rcurly()) 313 && (nextToken == null || !TokenUtil.areOnSameLine(details.rcurly(), nextToken) 314 || isRightcurlyFollowedBySemicolon(details)); 315 } 316 317 /** 318 * Checks whether the right curly is followed by a semicolon. 319 * 320 * @param details details for validation. 321 * @return true if the right curly is followed by a semicolon. 322 */ 323 private static boolean isRightcurlyFollowedBySemicolon(Details details) { 324 return details.nextToken().getType() == TokenTypes.SEMI; 325 } 326 327 /** 328 * Checks if right curly has line break before. 329 * 330 * @param rightCurly right curly token. 331 * @return true, if right curly has line break before. 332 */ 333 private static boolean hasLineBreakBefore(DetailAST rightCurly) { 334 DetailAST previousToken = rightCurly.getPreviousSibling(); 335 if (previousToken == null) { 336 previousToken = rightCurly.getParent(); 337 } 338 return !TokenUtil.areOnSameLine(rightCurly, previousToken); 339 } 340 341 /** 342 * Structure that contains all details for validation. 343 * 344 * @param lcurly the left curly token being analysed 345 * @param rcurly the matching right curly token 346 * @param nextToken the token following the right curly 347 * @param shouldCheckLastRcurly flag that indicates if the last right curly should be checked 348 */ 349 private record Details(DetailAST lcurly, DetailAST rcurly, 350 DetailAST nextToken, boolean shouldCheckLastRcurly) { 351 352 /** 353 * Token types that identify tokens that will never have SLIST in their AST. 354 */ 355 private static final int[] TOKENS_WITH_NO_CHILD_SLIST = { 356 TokenTypes.CLASS_DEF, 357 TokenTypes.ENUM_DEF, 358 TokenTypes.ANNOTATION_DEF, 359 TokenTypes.INTERFACE_DEF, 360 TokenTypes.RECORD_DEF, 361 }; 362 363 /** 364 * Collects validation Details. 365 * 366 * @param ast a {@code DetailAST} value 367 * @return object containing all details to make a validation 368 */ 369 private static Details getDetails(DetailAST ast) { 370 return switch (ast.getType()) { 371 case TokenTypes.LITERAL_TRY, TokenTypes.LITERAL_CATCH -> getDetailsForTryCatch(ast); 372 case TokenTypes.LITERAL_IF -> getDetailsForIf(ast); 373 case TokenTypes.LITERAL_DO -> getDetailsForDoLoops(ast); 374 case TokenTypes.LITERAL_SWITCH -> getDetailsForSwitch(ast); 375 case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> 376 getDetailsForCaseOrDefault(ast); 377 default -> getDetailsForOthers(ast); 378 }; 379 } 380 381 /** 382 * Collects details about switch statements and expressions. 383 * 384 * @param switchNode switch statement or expression to gather details about 385 * @return new Details about given switch statement or expression 386 */ 387 private static Details getDetailsForSwitch(DetailAST switchNode) { 388 final DetailAST lcurly = switchNode.findFirstToken(TokenTypes.LCURLY); 389 final DetailAST rcurly; 390 DetailAST nextToken = null; 391 // skipping switch expression as check only handles statements 392 if (isSwitchExpression(switchNode)) { 393 rcurly = null; 394 } 395 else { 396 rcurly = switchNode.getLastChild(); 397 nextToken = getNextToken(switchNode); 398 } 399 return new Details(lcurly, rcurly, nextToken, true); 400 } 401 402 /** 403 * Collects details about case and default statements. 404 * 405 * @param caseOrDefaultNode case or default statement to gather details about 406 * @return new Details about given case or default statement 407 */ 408 private static Details getDetailsForCaseOrDefault(DetailAST caseOrDefaultNode) { 409 final DetailAST caseOrDefaultParent = caseOrDefaultNode.getParent(); 410 final int parentType = caseOrDefaultParent.getType(); 411 final Optional<DetailAST> lcurly; 412 final DetailAST statementList; 413 414 if (parentType == TokenTypes.SWITCH_RULE) { 415 statementList = caseOrDefaultParent.findFirstToken(TokenTypes.SLIST); 416 lcurly = Optional.ofNullable(statementList); 417 } 418 else { 419 statementList = caseOrDefaultNode.getNextSibling(); 420 lcurly = Optional.ofNullable(statementList) 421 .map(DetailAST::getFirstChild) 422 .filter(node -> node.getType() == TokenTypes.SLIST); 423 } 424 final DetailAST rcurly = lcurly.map(DetailAST::getLastChild) 425 .filter(child -> !isSwitchExpression(caseOrDefaultParent)) 426 .orElse(null); 427 final Optional<DetailAST> nextToken = 428 Optional.ofNullable(lcurly.map(DetailAST::getNextSibling) 429 .orElseGet(() -> getNextToken(caseOrDefaultParent))); 430 431 return new Details(lcurly.orElse(null), rcurly, nextToken.orElse(null), true); 432 } 433 434 /** 435 * Check whether switch is expression or not. 436 * 437 * @param switchNode switch statement or expression to provide detail 438 * @return true if it is a switch expression 439 */ 440 private static boolean isSwitchExpression(DetailAST switchNode) { 441 DetailAST currentNode = switchNode; 442 boolean ans = false; 443 444 while (currentNode != null) { 445 if (currentNode.getType() == TokenTypes.EXPR) { 446 ans = true; 447 } 448 currentNode = currentNode.getParent(); 449 } 450 return ans; 451 } 452 453 /** 454 * Collects validation details for LITERAL_TRY, and LITERAL_CATCH. 455 * 456 * @param ast a {@code DetailAST} value 457 * @return object containing all details to make a validation 458 */ 459 private static Details getDetailsForTryCatch(DetailAST ast) { 460 final DetailAST lcurly; 461 DetailAST nextToken; 462 final int tokenType = ast.getType(); 463 if (tokenType == TokenTypes.LITERAL_TRY) { 464 if (ast.getFirstChild().getType() == TokenTypes.RESOURCE_SPECIFICATION) { 465 lcurly = ast.getFirstChild().getNextSibling(); 466 } 467 else { 468 lcurly = ast.getFirstChild(); 469 } 470 nextToken = lcurly.getNextSibling(); 471 } 472 else { 473 nextToken = ast.getNextSibling(); 474 lcurly = ast.getLastChild(); 475 } 476 477 final boolean shouldCheckLastRcurly; 478 if (nextToken == null) { 479 shouldCheckLastRcurly = true; 480 nextToken = getNextToken(ast); 481 } 482 else { 483 shouldCheckLastRcurly = false; 484 } 485 486 final DetailAST rcurly = lcurly.getLastChild(); 487 return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly); 488 } 489 490 /** 491 * Collects validation details for LITERAL_IF. 492 * 493 * @param ast a {@code DetailAST} value 494 * @return object containing all details to make a validation 495 */ 496 private static Details getDetailsForIf(DetailAST ast) { 497 final boolean shouldCheckLastRcurly; 498 final DetailAST lcurly; 499 DetailAST nextToken = ast.findFirstToken(TokenTypes.LITERAL_ELSE); 500 501 if (nextToken == null) { 502 shouldCheckLastRcurly = true; 503 nextToken = getNextToken(ast); 504 lcurly = ast.getLastChild(); 505 } 506 else { 507 shouldCheckLastRcurly = false; 508 lcurly = nextToken.getPreviousSibling(); 509 } 510 511 DetailAST rcurly = null; 512 if (lcurly.getType() == TokenTypes.SLIST) { 513 rcurly = lcurly.getLastChild(); 514 } 515 return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly); 516 } 517 518 /** 519 * Collects validation details for CLASS_DEF, RECORD_DEF, METHOD DEF, CTOR_DEF, STATIC_INIT, 520 * INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, and COMPACT_CTOR_DEF. 521 * 522 * @param ast a {@code DetailAST} value 523 * @return an object containing all details to make a validation 524 */ 525 private static Details getDetailsForOthers(DetailAST ast) { 526 DetailAST rcurly = null; 527 final DetailAST lcurly; 528 final int tokenType = ast.getType(); 529 if (isTokenWithNoChildSlist(tokenType)) { 530 final DetailAST child = ast.getLastChild(); 531 lcurly = child; 532 rcurly = child.getLastChild(); 533 } 534 else { 535 lcurly = ast.findFirstToken(TokenTypes.SLIST); 536 if (lcurly != null) { 537 // SLIST could be absent if method is abstract 538 rcurly = lcurly.getLastChild(); 539 } 540 } 541 return new Details(lcurly, rcurly, getNextToken(ast), true); 542 } 543 544 /** 545 * Tests whether the provided tokenType will never have a SLIST as child in its AST. 546 * Like CLASS_DEF, ANNOTATION_DEF etc. 547 * 548 * @param tokenType the tokenType to test against. 549 * @return weather provided tokenType is definition token. 550 */ 551 private static boolean isTokenWithNoChildSlist(int tokenType) { 552 return Arrays.stream(TOKENS_WITH_NO_CHILD_SLIST).anyMatch(token -> token == tokenType); 553 } 554 555 /** 556 * Collects validation details for LITERAL_DO loops' tokens. 557 * 558 * @param ast a {@code DetailAST} value 559 * @return an object containing all details to make a validation 560 */ 561 private static Details getDetailsForDoLoops(DetailAST ast) { 562 final DetailAST lcurly = ast.findFirstToken(TokenTypes.SLIST); 563 final DetailAST nextToken = ast.findFirstToken(TokenTypes.DO_WHILE); 564 DetailAST rcurly = null; 565 if (lcurly != null) { 566 rcurly = lcurly.getLastChild(); 567 } 568 return new Details(lcurly, rcurly, nextToken, false); 569 } 570 571 /** 572 * Finds next token after the given one. 573 * 574 * @param ast the given node. 575 * @return the token which represents next lexical item. 576 */ 577 private static DetailAST getNextToken(DetailAST ast) { 578 DetailAST next = null; 579 DetailAST parent = ast; 580 while (next == null && parent != null) { 581 next = parent.getNextSibling(); 582 parent = parent.getParent(); 583 } 584 return next; 585 } 586 } 587 588}