1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.blocks;
21
22 import java.util.Arrays;
23 import java.util.Locale;
24 import java.util.Optional;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
32
33 /**
34 * <div>
35 * Checks the placement of right curly braces (<code>'}'</code>) for code blocks. This check
36 * supports if-else, try-catch-finally blocks, switch statements, switch cases, switch default,
37 * while-loops, for-loops, method definitions, class definitions, constructor definitions,
38 * instance, static initialization blocks, annotation definitions and enum definitions.
39 * For right curly brace of expression blocks of arrays, lambdas and class instances
40 * please follow issue
41 * <a href="https://github.com/checkstyle/checkstyle/issues/5945">#5945</a>.
42 * For right curly brace of enum constant please follow issue
43 * <a href="https://github.com/checkstyle/checkstyle/issues/7519">#7519</a>.
44 * </div>
45 *
46 * @since 3.0
47 */
48 @StatelessCheck
49 public class RightCurlyCheck extends AbstractCheck {
50
51 /**
52 * A key is pointing to the warning message text in "messages.properties"
53 * file.
54 */
55 public static final String MSG_KEY_LINE_BREAK_BEFORE = "line.break.before";
56
57 /**
58 * A key is pointing to the warning message text in "messages.properties"
59 * file.
60 */
61 public static final String MSG_KEY_LINE_ALONE = "line.alone";
62
63 /**
64 * A key is pointing to the warning message text in "messages.properties"
65 * file.
66 */
67 public static final String MSG_KEY_LINE_SAME = "line.same";
68
69 /**
70 * Specify the policy on placement of a right curly brace (<code>'}'</code>).
71 */
72 private RightCurlyOption option = RightCurlyOption.SAME;
73
74 /**
75 * Creates a new {@code RightCurlyCheck} instance.
76 */
77 public RightCurlyCheck() {
78 // no code by default
79 }
80
81 /**
82 * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>).
83 *
84 * @param optionStr string to decode option from
85 * @throws IllegalArgumentException if unable to decode
86 * @since 3.0
87 */
88 public void setOption(String optionStr) {
89 option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
90 }
91
92 @Override
93 public int[] getDefaultTokens() {
94 return new int[] {
95 TokenTypes.LITERAL_TRY,
96 TokenTypes.LITERAL_CATCH,
97 TokenTypes.LITERAL_FINALLY,
98 TokenTypes.LITERAL_IF,
99 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:
265 * {@snippet lang="text" :
266 * Map<String, String> map = new LinkedHashMap<>() {{
267 * put("alpha", "man");
268 * }}; // no violation
269 * }
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 (TokenUtil.isOfType(tokenAfterNextToken, TokenTypes.SEMI)) {
278 final DetailAST rcurly = details.rcurly();
279 final DetailAST tokenAfterSemi = Details.getNextToken(tokenAfterNextToken);
280 skipDoubleBraceInstInit = tokenAfterSemi != null
281 && rcurly.getParent().getParent()
282 .getType() == TokenTypes.INSTANCE_INIT
283 && details.nextToken().getType() == TokenTypes.RCURLY
284 && !TokenUtil.areOnSameLine(rcurly, tokenAfterSemi);
285 }
286 return skipDoubleBraceInstInit;
287 }
288
289 /**
290 * Checks whether block has a single-line format and is alone on a line.
291 *
292 * @param details for validation.
293 * @return true if block has single-line format and is alone on a line.
294 */
295 private static boolean isBlockAloneOnSingleLine(Details details) {
296 DetailAST nextToken = details.nextToken();
297
298 while (nextToken != null && nextToken.getType() == TokenTypes.LITERAL_ELSE) {
299 nextToken = Details.getNextToken(nextToken);
300 }
301
302 // sibling tokens should be allowed on a single line
303 final int[] tokensWithBlockSibling = {
304 TokenTypes.DO_WHILE,
305 TokenTypes.LITERAL_FINALLY,
306 TokenTypes.LITERAL_CATCH,
307 };
308
309 if (TokenUtil.isOfType(nextToken, tokensWithBlockSibling)) {
310 final DetailAST parent = nextToken.getParent();
311 nextToken = Details.getNextToken(parent);
312 }
313
314 return TokenUtil.areOnSameLine(details.lcurly(), details.rcurly())
315 && (nextToken == null || !TokenUtil.areOnSameLine(details.rcurly(), nextToken)
316 || isRightcurlyFollowedBySemicolon(details));
317 }
318
319 /**
320 * Checks whether the right curly is followed by a semicolon.
321 *
322 * @param details details for validation.
323 * @return true if the right curly is followed by a semicolon.
324 */
325 private static boolean isRightcurlyFollowedBySemicolon(Details details) {
326 return details.nextToken().getType() == TokenTypes.SEMI;
327 }
328
329 /**
330 * Checks if right curly has line break before.
331 *
332 * @param rightCurly right curly token.
333 * @return true, if right curly has line break before.
334 */
335 private static boolean hasLineBreakBefore(DetailAST rightCurly) {
336 DetailAST previousToken = rightCurly.getPreviousSibling();
337 if (previousToken == null) {
338 previousToken = rightCurly.getParent();
339 }
340 return !TokenUtil.areOnSameLine(rightCurly, previousToken);
341 }
342
343 /**
344 * Structure that contains all details for validation.
345 *
346 * @param lcurly the left curly token being analysed
347 * @param rcurly the matching right curly token
348 * @param nextToken the token following the right curly
349 * @param shouldCheckLastRcurly flag that indicates if the last right curly should be checked
350 */
351 private record Details(DetailAST lcurly, DetailAST rcurly,
352 DetailAST nextToken, boolean shouldCheckLastRcurly) {
353
354 /**
355 * Token types that identify tokens that will never have SLIST in their AST.
356 */
357 private static final int[] TOKENS_WITH_NO_CHILD_SLIST = {
358 TokenTypes.CLASS_DEF,
359 TokenTypes.ENUM_DEF,
360 TokenTypes.ANNOTATION_DEF,
361 TokenTypes.INTERFACE_DEF,
362 TokenTypes.RECORD_DEF,
363 };
364
365 /**
366 * Collects validation Details.
367 *
368 * @param ast a {@code DetailAST} value
369 * @return object containing all details to make a validation
370 */
371 private static Details getDetails(DetailAST ast) {
372 return switch (ast.getType()) {
373 case TokenTypes.LITERAL_TRY, TokenTypes.LITERAL_CATCH -> getDetailsForTryCatch(ast);
374 case TokenTypes.LITERAL_IF -> getDetailsForIf(ast);
375 case TokenTypes.LITERAL_DO -> getDetailsForDoLoops(ast);
376 case TokenTypes.LITERAL_SWITCH -> getDetailsForSwitch(ast);
377 case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT ->
378 getDetailsForCaseOrDefault(ast);
379 default -> getDetailsForOthers(ast);
380 };
381 }
382
383 /**
384 * Collects details about switch statements and expressions.
385 *
386 * @param switchNode switch statement or expression to gather details about
387 * @return new Details about given switch statement or expression
388 */
389 private static Details getDetailsForSwitch(DetailAST switchNode) {
390 final DetailAST lcurly = switchNode.findFirstToken(TokenTypes.LCURLY);
391 final DetailAST rcurly;
392 DetailAST nextToken = null;
393 // skipping switch expression as check only handles statements
394 if (isSwitchExpression(switchNode)) {
395 rcurly = null;
396 }
397 else {
398 rcurly = switchNode.getLastChild();
399 nextToken = getNextToken(switchNode);
400 }
401 return new Details(lcurly, rcurly, nextToken, true);
402 }
403
404 /**
405 * Collects details about case and default statements.
406 *
407 * @param caseOrDefaultNode case or default statement to gather details about
408 * @return new Details about given case or default statement
409 */
410 private static Details getDetailsForCaseOrDefault(DetailAST caseOrDefaultNode) {
411 final DetailAST caseOrDefaultParent = caseOrDefaultNode.getParent();
412 final int parentType = caseOrDefaultParent.getType();
413 final Optional<DetailAST> lcurly;
414 final DetailAST statementList;
415
416 if (parentType == TokenTypes.SWITCH_RULE) {
417 statementList = caseOrDefaultParent.findFirstToken(TokenTypes.SLIST);
418 lcurly = Optional.ofNullable(statementList);
419 }
420 else {
421 statementList = caseOrDefaultNode.getNextSibling();
422 lcurly = Optional.ofNullable(statementList)
423 .map(DetailAST::getFirstChild)
424 .filter(node -> node.getType() == TokenTypes.SLIST);
425 }
426 final DetailAST rcurly = lcurly.map(DetailAST::getLastChild)
427 .filter(child -> !isSwitchExpression(caseOrDefaultParent))
428 .orElse(null);
429 final Optional<DetailAST> nextToken =
430 Optional.ofNullable(lcurly.map(DetailAST::getNextSibling)
431 .orElseGet(() -> getNextToken(caseOrDefaultParent)));
432
433 return new Details(lcurly.orElse(null), rcurly, nextToken.orElse(null), true);
434 }
435
436 /**
437 * Check whether switch is expression or not.
438 *
439 * @param switchNode switch statement or expression to provide detail
440 * @return true if it is a switch expression
441 */
442 private static boolean isSwitchExpression(DetailAST switchNode) {
443 DetailAST currentNode = switchNode;
444 boolean ans = false;
445
446 while (currentNode != null) {
447 if (currentNode.getType() == TokenTypes.EXPR) {
448 ans = true;
449 }
450 currentNode = currentNode.getParent();
451 }
452 return ans;
453 }
454
455 /**
456 * Collects validation details for LITERAL_TRY, and LITERAL_CATCH.
457 *
458 * @param ast a {@code DetailAST} value
459 * @return object containing all details to make a validation
460 */
461 private static Details getDetailsForTryCatch(DetailAST ast) {
462 final DetailAST lcurly;
463 DetailAST nextToken;
464 final int tokenType = ast.getType();
465 if (tokenType == TokenTypes.LITERAL_TRY) {
466 if (ast.getFirstChild().getType() == TokenTypes.RESOURCE_SPECIFICATION) {
467 lcurly = ast.getFirstChild().getNextSibling();
468 }
469 else {
470 lcurly = ast.getFirstChild();
471 }
472 nextToken = lcurly.getNextSibling();
473 }
474 else {
475 nextToken = ast.getNextSibling();
476 lcurly = ast.getLastChild();
477 }
478
479 final boolean shouldCheckLastRcurly;
480 if (nextToken == null) {
481 shouldCheckLastRcurly = true;
482 nextToken = getNextToken(ast);
483 }
484 else {
485 shouldCheckLastRcurly = false;
486 }
487
488 final DetailAST rcurly = lcurly.getLastChild();
489 return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
490 }
491
492 /**
493 * Collects validation details for LITERAL_IF.
494 *
495 * @param ast a {@code DetailAST} value
496 * @return object containing all details to make a validation
497 */
498 private static Details getDetailsForIf(DetailAST ast) {
499 final boolean shouldCheckLastRcurly;
500 final DetailAST lcurly;
501 DetailAST nextToken = ast.findFirstToken(TokenTypes.LITERAL_ELSE);
502
503 if (nextToken == null) {
504 shouldCheckLastRcurly = true;
505 nextToken = getNextToken(ast);
506 lcurly = ast.getLastChild();
507 }
508 else {
509 shouldCheckLastRcurly = false;
510 lcurly = nextToken.getPreviousSibling();
511 }
512
513 DetailAST rcurly = null;
514 if (lcurly.getType() == TokenTypes.SLIST) {
515 rcurly = lcurly.getLastChild();
516 }
517 return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
518 }
519
520 /**
521 * Collects validation details for CLASS_DEF, RECORD_DEF, METHOD DEF, CTOR_DEF, STATIC_INIT,
522 * INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, and COMPACT_CTOR_DEF.
523 *
524 * @param ast a {@code DetailAST} value
525 * @return an object containing all details to make a validation
526 */
527 private static Details getDetailsForOthers(DetailAST ast) {
528 DetailAST rcurly = null;
529 final DetailAST lcurly;
530 final int tokenType = ast.getType();
531 if (isTokenWithNoChildSlist(tokenType)) {
532 final DetailAST child = ast.getLastChild();
533 lcurly = child;
534 rcurly = child.getLastChild();
535 }
536 else {
537 lcurly = ast.findFirstToken(TokenTypes.SLIST);
538 if (lcurly != null) {
539 // SLIST could be absent if method is abstract
540 rcurly = lcurly.getLastChild();
541 }
542 }
543 return new Details(lcurly, rcurly, getNextToken(ast), true);
544 }
545
546 /**
547 * Tests whether the provided tokenType will never have a SLIST as child in its AST.
548 * Like CLASS_DEF, ANNOTATION_DEF etc.
549 *
550 * @param tokenType the tokenType to test against.
551 * @return weather provided tokenType is definition token.
552 */
553 private static boolean isTokenWithNoChildSlist(int tokenType) {
554 return Arrays.stream(TOKENS_WITH_NO_CHILD_SLIST).anyMatch(token -> token == tokenType);
555 }
556
557 /**
558 * Collects validation details for LITERAL_DO loops' tokens.
559 *
560 * @param ast a {@code DetailAST} value
561 * @return an object containing all details to make a validation
562 */
563 private static Details getDetailsForDoLoops(DetailAST ast) {
564 final DetailAST lcurly = ast.findFirstToken(TokenTypes.SLIST);
565 final DetailAST nextToken = ast.findFirstToken(TokenTypes.DO_WHILE);
566 DetailAST rcurly = null;
567 if (lcurly != null) {
568 rcurly = lcurly.getLastChild();
569 }
570 return new Details(lcurly, rcurly, nextToken, false);
571 }
572
573 /**
574 * Finds next token after the given one.
575 *
576 * @param ast the given node.
577 * @return the token which represents next lexical item.
578 */
579 private static DetailAST getNextToken(DetailAST ast) {
580 DetailAST next = null;
581 DetailAST parent = ast;
582 while (next == null && parent != null) {
583 next = parent.getNextSibling();
584 parent = parent.getParent();
585 }
586 return next;
587 }
588 }
589
590 }