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"
053     * file.
054     */
055    public static final String MSG_KEY_LINE_BREAK_BEFORE = "line.break.before";
056
057    /**
058     * A key is pointing to the warning message text in "messages.properties"
059     * file.
060     */
061    public static final String MSG_KEY_LINE_ALONE = "line.alone";
062
063    /**
064     * A key is pointing to the warning message text in "messages.properties"
065     * file.
066     */
067    public static final String MSG_KEY_LINE_SAME = "line.same";
068
069    /**
070     * Specify the policy on placement of a right curly brace (<code>'}'</code>).
071     */
072    private RightCurlyOption option = RightCurlyOption.SAME;
073
074    /**
075     * Creates a new {@code RightCurlyCheck} instance.
076     */
077    public RightCurlyCheck() {
078        // no code by default
079    }
080
081    /**
082     * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>).
083     *
084     * @param optionStr string to decode option from
085     * @throws IllegalArgumentException if unable to decode
086     * @since 3.0
087     */
088    public void setOption(String optionStr) {
089        option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
090    }
091
092    @Override
093    public int[] getDefaultTokens() {
094        return new int[] {
095            TokenTypes.LITERAL_TRY,
096            TokenTypes.LITERAL_CATCH,
097            TokenTypes.LITERAL_FINALLY,
098            TokenTypes.LITERAL_IF,
099            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}