View Javadoc
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     private static final class Details {
337 
338         /**
339          * Token types that identify tokens that will never have SLIST in their AST.
340          */
341         private static final int[] TOKENS_WITH_NO_CHILD_SLIST = {
342             TokenTypes.CLASS_DEF,
343             TokenTypes.ENUM_DEF,
344             TokenTypes.ANNOTATION_DEF,
345             TokenTypes.INTERFACE_DEF,
346             TokenTypes.RECORD_DEF,
347         };
348 
349         /** Right curly. */
350         private final DetailAST rcurly;
351         /** Left curly. */
352         private final DetailAST lcurly;
353         /** Next token. */
354         private final DetailAST nextToken;
355         /** Should check last right curly. */
356         private final boolean shouldCheckLastRcurly;
357 
358         /**
359          * Constructor.
360          *
361          * @param lcurly the lcurly of the token whose details are being collected
362          * @param rcurly the rcurly of the token whose details are being collected
363          * @param nextToken the token after the token whose details are being collected
364          * @param shouldCheckLastRcurly boolean value to determine if to check last rcurly
365          */
366         private Details(DetailAST lcurly, DetailAST rcurly,
367                         DetailAST nextToken, boolean shouldCheckLastRcurly) {
368             this.lcurly = lcurly;
369             this.rcurly = rcurly;
370             this.nextToken = nextToken;
371             this.shouldCheckLastRcurly = shouldCheckLastRcurly;
372         }
373 
374         /**
375          * Collects validation Details.
376          *
377          * @param ast a {@code DetailAST} value
378          * @return object containing all details to make a validation
379          */
380         private static Details getDetails(DetailAST ast) {
381             return switch (ast.getType()) {
382                 case TokenTypes.LITERAL_TRY, TokenTypes.LITERAL_CATCH -> getDetailsForTryCatch(ast);
383                 case TokenTypes.LITERAL_IF -> getDetailsForIf(ast);
384                 case TokenTypes.LITERAL_DO -> getDetailsForDoLoops(ast);
385                 case TokenTypes.LITERAL_SWITCH -> getDetailsForSwitch(ast);
386                 case TokenTypes.LITERAL_CASE -> getDetailsForCase(ast);
387                 default -> getDetailsForOthers(ast);
388             };
389         }
390 
391         /**
392          * Collects details about switch statements and expressions.
393          *
394          * @param switchNode switch statement or expression to gather details about
395          * @return new Details about given switch statement or expression
396          */
397         private static Details getDetailsForSwitch(DetailAST switchNode) {
398             final DetailAST lcurly = switchNode.findFirstToken(TokenTypes.LCURLY);
399             final DetailAST rcurly;
400             DetailAST nextToken = null;
401             // skipping switch expression as check only handles statements
402             if (isSwitchExpression(switchNode)) {
403                 rcurly = null;
404             }
405             else {
406                 rcurly = switchNode.getLastChild();
407                 nextToken = getNextToken(switchNode);
408             }
409             return new Details(lcurly, rcurly, nextToken, true);
410         }
411 
412         /**
413          * Collects details about case statements.
414          *
415          * @param caseNode case statement to gather details about
416          * @return new Details about given case statement
417          */
418         private static Details getDetailsForCase(DetailAST caseNode) {
419             final DetailAST caseParent = caseNode.getParent();
420             final int parentType = caseParent.getType();
421             final Optional<DetailAST> lcurly;
422             final DetailAST statementList;
423 
424             if (parentType == TokenTypes.SWITCH_RULE) {
425                 statementList = caseParent.findFirstToken(TokenTypes.SLIST);
426                 lcurly = Optional.ofNullable(statementList);
427             }
428             else {
429                 statementList = caseNode.getNextSibling();
430                 lcurly = Optional.ofNullable(statementList)
431                          .map(DetailAST::getFirstChild)
432                          .filter(node -> node.getType() == TokenTypes.SLIST);
433             }
434             final DetailAST rcurly = lcurly.map(DetailAST::getLastChild)
435                     .filter(child -> !isSwitchExpression(caseParent))
436                     .orElse(null);
437             final Optional<DetailAST> nextToken =
438                     Optional.ofNullable(lcurly.map(DetailAST::getNextSibling)
439                     .orElseGet(() -> getNextToken(caseParent)));
440 
441             return new Details(lcurly.orElse(null), rcurly, nextToken.orElse(null), true);
442         }
443 
444         /**
445          * Check whether switch is expression or not.
446          *
447          * @param switchNode switch statement or expression to provide detail
448          * @return true if it is a switch expression
449          */
450         private static boolean isSwitchExpression(DetailAST switchNode) {
451             DetailAST currentNode = switchNode;
452             boolean ans = false;
453 
454             while (currentNode != null) {
455                 if (currentNode.getType() == TokenTypes.EXPR) {
456                     ans = true;
457                 }
458                 currentNode = currentNode.getParent();
459             }
460             return ans;
461         }
462 
463         /**
464          * Collects validation details for LITERAL_TRY, and LITERAL_CATCH.
465          *
466          * @param ast a {@code DetailAST} value
467          * @return object containing all details to make a validation
468          */
469         private static Details getDetailsForTryCatch(DetailAST ast) {
470             final DetailAST lcurly;
471             DetailAST nextToken;
472             final int tokenType = ast.getType();
473             if (tokenType == TokenTypes.LITERAL_TRY) {
474                 if (ast.getFirstChild().getType() == TokenTypes.RESOURCE_SPECIFICATION) {
475                     lcurly = ast.getFirstChild().getNextSibling();
476                 }
477                 else {
478                     lcurly = ast.getFirstChild();
479                 }
480                 nextToken = lcurly.getNextSibling();
481             }
482             else {
483                 nextToken = ast.getNextSibling();
484                 lcurly = ast.getLastChild();
485             }
486 
487             final boolean shouldCheckLastRcurly;
488             if (nextToken == null) {
489                 shouldCheckLastRcurly = true;
490                 nextToken = getNextToken(ast);
491             }
492             else {
493                 shouldCheckLastRcurly = false;
494             }
495 
496             final DetailAST rcurly = lcurly.getLastChild();
497             return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
498         }
499 
500         /**
501          * Collects validation details for LITERAL_IF.
502          *
503          * @param ast a {@code DetailAST} value
504          * @return object containing all details to make a validation
505          */
506         private static Details getDetailsForIf(DetailAST ast) {
507             final boolean shouldCheckLastRcurly;
508             final DetailAST lcurly;
509             DetailAST nextToken = ast.findFirstToken(TokenTypes.LITERAL_ELSE);
510 
511             if (nextToken == null) {
512                 shouldCheckLastRcurly = true;
513                 nextToken = getNextToken(ast);
514                 lcurly = ast.getLastChild();
515             }
516             else {
517                 shouldCheckLastRcurly = false;
518                 lcurly = nextToken.getPreviousSibling();
519             }
520 
521             DetailAST rcurly = null;
522             if (lcurly.getType() == TokenTypes.SLIST) {
523                 rcurly = lcurly.getLastChild();
524             }
525             return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
526         }
527 
528         /**
529          * Collects validation details for CLASS_DEF, RECORD_DEF, METHOD DEF, CTOR_DEF, STATIC_INIT,
530          * INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, and COMPACT_CTOR_DEF.
531          *
532          * @param ast a {@code DetailAST} value
533          * @return an object containing all details to make a validation
534          */
535         private static Details getDetailsForOthers(DetailAST ast) {
536             DetailAST rcurly = null;
537             final DetailAST lcurly;
538             final int tokenType = ast.getType();
539             if (isTokenWithNoChildSlist(tokenType)) {
540                 final DetailAST child = ast.getLastChild();
541                 lcurly = child;
542                 rcurly = child.getLastChild();
543             }
544             else {
545                 lcurly = ast.findFirstToken(TokenTypes.SLIST);
546                 if (lcurly != null) {
547                     // SLIST could be absent if method is abstract
548                     rcurly = lcurly.getLastChild();
549                 }
550             }
551             return new Details(lcurly, rcurly, getNextToken(ast), true);
552         }
553 
554         /**
555          * Tests whether the provided tokenType will never have a SLIST as child in its AST.
556          * Like CLASS_DEF, ANNOTATION_DEF etc.
557          *
558          * @param tokenType the tokenType to test against.
559          * @return weather provided tokenType is definition token.
560          */
561         private static boolean isTokenWithNoChildSlist(int tokenType) {
562             return Arrays.stream(TOKENS_WITH_NO_CHILD_SLIST).anyMatch(token -> token == tokenType);
563         }
564 
565         /**
566          * Collects validation details for LITERAL_DO loops' tokens.
567          *
568          * @param ast a {@code DetailAST} value
569          * @return an object containing all details to make a validation
570          */
571         private static Details getDetailsForDoLoops(DetailAST ast) {
572             final DetailAST lcurly = ast.findFirstToken(TokenTypes.SLIST);
573             final DetailAST nextToken = ast.findFirstToken(TokenTypes.DO_WHILE);
574             DetailAST rcurly = null;
575             if (lcurly != null) {
576                 rcurly = lcurly.getLastChild();
577             }
578             return new Details(lcurly, rcurly, nextToken, false);
579         }
580 
581         /**
582          * Finds next token after the given one.
583          *
584          * @param ast the given node.
585          * @return the token which represents next lexical item.
586          */
587         private static DetailAST getNextToken(DetailAST ast) {
588             DetailAST next = null;
589             DetailAST parent = ast;
590             while (next == null && parent != null) {
591                 next = parent.getNextSibling();
592                 parent = parent.getParent();
593             }
594             return next;
595         }
596     }
597 }