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