001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.blocks;
021
022import java.util.Arrays;
023import java.util.Locale;
024import java.util.Optional;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
032
033/**
034 * <div>
035 * Checks the placement of right curly braces (<code>'}'</code>) for code blocks. This check
036 * supports if-else, try-catch-finally blocks, switch statements, switch cases, while-loops,
037 *  for-loops, method definitions, class definitions, constructor definitions,
038 * instance, static initialization blocks, annotation definitions and enum definitions.
039 * For right curly brace of expression blocks of arrays, lambdas and class instances
040 * please follow issue
041 * <a href="https://github.com/checkstyle/checkstyle/issues/5945">#5945</a>.
042 * For right curly brace of enum constant please follow issue
043 * <a href="https://github.com/checkstyle/checkstyle/issues/7519">#7519</a>.
044 * </div>
045 *
046 *
047 * @since 3.0
048 */
049@StatelessCheck
050public class RightCurlyCheck extends AbstractCheck {
051
052    /**
053     * A key is pointing to the warning message text in "messages.properties"
054     * file.
055     */
056    public static final String MSG_KEY_LINE_BREAK_BEFORE = "line.break.before";
057
058    /**
059     * A key is pointing to the warning message text in "messages.properties"
060     * file.
061     */
062    public static final String MSG_KEY_LINE_ALONE = "line.alone";
063
064    /**
065     * A key is pointing to the warning message text in "messages.properties"
066     * file.
067     */
068    public static final String MSG_KEY_LINE_SAME = "line.same";
069
070    /**
071     * Specify the policy on placement of a right curly brace (<code>'}'</code>).
072     */
073    private RightCurlyOption option = RightCurlyOption.SAME;
074
075    /**
076     * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>).
077     *
078     * @param optionStr string to decode option from
079     * @throws IllegalArgumentException if unable to decode
080     * @since 3.0
081     */
082    public void setOption(String optionStr) {
083        option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
084    }
085
086    @Override
087    public int[] getDefaultTokens() {
088        return new int[] {
089            TokenTypes.LITERAL_TRY,
090            TokenTypes.LITERAL_CATCH,
091            TokenTypes.LITERAL_FINALLY,
092            TokenTypes.LITERAL_IF,
093            TokenTypes.LITERAL_ELSE,
094        };
095    }
096
097    @Override
098    public int[] getAcceptableTokens() {
099        return new int[] {
100            TokenTypes.LITERAL_TRY,
101            TokenTypes.LITERAL_CATCH,
102            TokenTypes.LITERAL_FINALLY,
103            TokenTypes.LITERAL_IF,
104            TokenTypes.LITERAL_ELSE,
105            TokenTypes.CLASS_DEF,
106            TokenTypes.METHOD_DEF,
107            TokenTypes.CTOR_DEF,
108            TokenTypes.LITERAL_FOR,
109            TokenTypes.LITERAL_WHILE,
110            TokenTypes.LITERAL_DO,
111            TokenTypes.STATIC_INIT,
112            TokenTypes.INSTANCE_INIT,
113            TokenTypes.ANNOTATION_DEF,
114            TokenTypes.ENUM_DEF,
115            TokenTypes.INTERFACE_DEF,
116            TokenTypes.RECORD_DEF,
117            TokenTypes.COMPACT_CTOR_DEF,
118            TokenTypes.LITERAL_SWITCH,
119            TokenTypes.LITERAL_CASE,
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    private static final class Details {
338
339        /**
340         * Token types that identify tokens that will never have SLIST in their AST.
341         */
342        private static final int[] TOKENS_WITH_NO_CHILD_SLIST = {
343            TokenTypes.CLASS_DEF,
344            TokenTypes.ENUM_DEF,
345            TokenTypes.ANNOTATION_DEF,
346            TokenTypes.INTERFACE_DEF,
347            TokenTypes.RECORD_DEF,
348        };
349
350        /** Right curly. */
351        private final DetailAST rcurly;
352        /** Left curly. */
353        private final DetailAST lcurly;
354        /** Next token. */
355        private final DetailAST nextToken;
356        /** Should check last right curly. */
357        private final boolean shouldCheckLastRcurly;
358
359        /**
360         * Constructor.
361         *
362         * @param lcurly the lcurly of the token whose details are being collected
363         * @param rcurly the rcurly of the token whose details are being collected
364         * @param nextToken the token after the token whose details are being collected
365         * @param shouldCheckLastRcurly boolean value to determine if to check last rcurly
366         */
367        private Details(DetailAST lcurly, DetailAST rcurly,
368                        DetailAST nextToken, boolean shouldCheckLastRcurly) {
369            this.lcurly = lcurly;
370            this.rcurly = rcurly;
371            this.nextToken = nextToken;
372            this.shouldCheckLastRcurly = shouldCheckLastRcurly;
373        }
374
375        /**
376         * Collects validation Details.
377         *
378         * @param ast a {@code DetailAST} value
379         * @return object containing all details to make a validation
380         */
381        private static Details getDetails(DetailAST ast) {
382            return switch (ast.getType()) {
383                case TokenTypes.LITERAL_TRY, TokenTypes.LITERAL_CATCH -> getDetailsForTryCatch(ast);
384                case TokenTypes.LITERAL_IF -> getDetailsForIf(ast);
385                case TokenTypes.LITERAL_DO -> getDetailsForDoLoops(ast);
386                case TokenTypes.LITERAL_SWITCH -> getDetailsForSwitch(ast);
387                case TokenTypes.LITERAL_CASE -> getDetailsForCase(ast);
388                default -> getDetailsForOthers(ast);
389            };
390        }
391
392        /**
393         * Collects details about switch statements and expressions.
394         *
395         * @param switchNode switch statement or expression to gather details about
396         * @return new Details about given switch statement or expression
397         */
398        private static Details getDetailsForSwitch(DetailAST switchNode) {
399            final DetailAST lcurly = switchNode.findFirstToken(TokenTypes.LCURLY);
400            final DetailAST rcurly;
401            DetailAST nextToken = null;
402            // skipping switch expression as check only handles statements
403            if (isSwitchExpression(switchNode)) {
404                rcurly = null;
405            }
406            else {
407                rcurly = switchNode.getLastChild();
408                nextToken = getNextToken(switchNode);
409            }
410            return new Details(lcurly, rcurly, nextToken, true);
411        }
412
413        /**
414         * Collects details about case statements.
415         *
416         * @param caseNode case statement to gather details about
417         * @return new Details about given case statement
418         */
419        private static Details getDetailsForCase(DetailAST caseNode) {
420            final DetailAST caseParent = caseNode.getParent();
421            final int parentType = caseParent.getType();
422            final Optional<DetailAST> lcurly;
423            final DetailAST statementList;
424
425            if (parentType == TokenTypes.SWITCH_RULE) {
426                statementList = caseParent.findFirstToken(TokenTypes.SLIST);
427                lcurly = Optional.ofNullable(statementList);
428            }
429            else {
430                statementList = caseNode.getNextSibling();
431                lcurly = Optional.ofNullable(statementList)
432                         .map(DetailAST::getFirstChild)
433                         .filter(node -> node.getType() == TokenTypes.SLIST);
434            }
435            final DetailAST rcurly = lcurly.map(DetailAST::getLastChild)
436                    .filter(child -> !isSwitchExpression(caseParent))
437                    .orElse(null);
438            final Optional<DetailAST> nextToken =
439                    Optional.ofNullable(lcurly.map(DetailAST::getNextSibling)
440                    .orElseGet(() -> getNextToken(caseParent)));
441
442            return new Details(lcurly.orElse(null), rcurly, nextToken.orElse(null), true);
443        }
444
445        /**
446         * Check whether switch is expression or not.
447         *
448         * @param switchNode switch statement or expression to provide detail
449         * @return true if it is a switch expression
450         */
451        private static boolean isSwitchExpression(DetailAST switchNode) {
452            DetailAST currentNode = switchNode;
453            boolean ans = false;
454
455            while (currentNode != null) {
456                if (currentNode.getType() == TokenTypes.EXPR) {
457                    ans = true;
458                }
459                currentNode = currentNode.getParent();
460            }
461            return ans;
462        }
463
464        /**
465         * Collects validation details for LITERAL_TRY, and LITERAL_CATCH.
466         *
467         * @param ast a {@code DetailAST} value
468         * @return object containing all details to make a validation
469         */
470        private static Details getDetailsForTryCatch(DetailAST ast) {
471            final DetailAST lcurly;
472            DetailAST nextToken;
473            final int tokenType = ast.getType();
474            if (tokenType == TokenTypes.LITERAL_TRY) {
475                if (ast.getFirstChild().getType() == TokenTypes.RESOURCE_SPECIFICATION) {
476                    lcurly = ast.getFirstChild().getNextSibling();
477                }
478                else {
479                    lcurly = ast.getFirstChild();
480                }
481                nextToken = lcurly.getNextSibling();
482            }
483            else {
484                nextToken = ast.getNextSibling();
485                lcurly = ast.getLastChild();
486            }
487
488            final boolean shouldCheckLastRcurly;
489            if (nextToken == null) {
490                shouldCheckLastRcurly = true;
491                nextToken = getNextToken(ast);
492            }
493            else {
494                shouldCheckLastRcurly = false;
495            }
496
497            final DetailAST rcurly = lcurly.getLastChild();
498            return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
499        }
500
501        /**
502         * Collects validation details for LITERAL_IF.
503         *
504         * @param ast a {@code DetailAST} value
505         * @return object containing all details to make a validation
506         */
507        private static Details getDetailsForIf(DetailAST ast) {
508            final boolean shouldCheckLastRcurly;
509            final DetailAST lcurly;
510            DetailAST nextToken = ast.findFirstToken(TokenTypes.LITERAL_ELSE);
511
512            if (nextToken == null) {
513                shouldCheckLastRcurly = true;
514                nextToken = getNextToken(ast);
515                lcurly = ast.getLastChild();
516            }
517            else {
518                shouldCheckLastRcurly = false;
519                lcurly = nextToken.getPreviousSibling();
520            }
521
522            DetailAST rcurly = null;
523            if (lcurly.getType() == TokenTypes.SLIST) {
524                rcurly = lcurly.getLastChild();
525            }
526            return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
527        }
528
529        /**
530         * Collects validation details for CLASS_DEF, RECORD_DEF, METHOD DEF, CTOR_DEF, STATIC_INIT,
531         * INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, and COMPACT_CTOR_DEF.
532         *
533         * @param ast a {@code DetailAST} value
534         * @return an object containing all details to make a validation
535         */
536        private static Details getDetailsForOthers(DetailAST ast) {
537            DetailAST rcurly = null;
538            final DetailAST lcurly;
539            final int tokenType = ast.getType();
540            if (isTokenWithNoChildSlist(tokenType)) {
541                final DetailAST child = ast.getLastChild();
542                lcurly = child;
543                rcurly = child.getLastChild();
544            }
545            else {
546                lcurly = ast.findFirstToken(TokenTypes.SLIST);
547                if (lcurly != null) {
548                    // SLIST could be absent if method is abstract
549                    rcurly = lcurly.getLastChild();
550                }
551            }
552            return new Details(lcurly, rcurly, getNextToken(ast), true);
553        }
554
555        /**
556         * Tests whether the provided tokenType will never have a SLIST as child in its AST.
557         * Like CLASS_DEF, ANNOTATION_DEF etc.
558         *
559         * @param tokenType the tokenType to test against.
560         * @return weather provided tokenType is definition token.
561         */
562        private static boolean isTokenWithNoChildSlist(int tokenType) {
563            return Arrays.stream(TOKENS_WITH_NO_CHILD_SLIST).anyMatch(token -> token == tokenType);
564        }
565
566        /**
567         * Collects validation details for LITERAL_DO loops' tokens.
568         *
569         * @param ast a {@code DetailAST} value
570         * @return an object containing all details to make a validation
571         */
572        private static Details getDetailsForDoLoops(DetailAST ast) {
573            final DetailAST lcurly = ast.findFirstToken(TokenTypes.SLIST);
574            final DetailAST nextToken = ast.findFirstToken(TokenTypes.DO_WHILE);
575            DetailAST rcurly = null;
576            if (lcurly != null) {
577                rcurly = lcurly.getLastChild();
578            }
579            return new Details(lcurly, rcurly, nextToken, false);
580        }
581
582        /**
583         * Finds next token after the given one.
584         *
585         * @param ast the given node.
586         * @return the token which represents next lexical item.
587         */
588        private static DetailAST getNextToken(DetailAST ast) {
589            DetailAST next = null;
590            DetailAST parent = ast;
591            while (next == null && parent != null) {
592                next = parent.getNextSibling();
593                parent = parent.getParent();
594            }
595            return next;
596        }
597    }
598}