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