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