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