View Javadoc
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       * 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             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 }