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 * @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     * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>).
076     *
077     * @param optionStr string to decode option from
078     * @throws IllegalArgumentException if unable to decode
079     * @since 3.0
080     */
081    public void setOption(String optionStr) {
082        option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
083    }
084
085    @Override
086    public int[] getDefaultTokens() {
087        return new int[] {
088            TokenTypes.LITERAL_TRY,
089            TokenTypes.LITERAL_CATCH,
090            TokenTypes.LITERAL_FINALLY,
091            TokenTypes.LITERAL_IF,
092            TokenTypes.LITERAL_ELSE,
093        };
094    }
095
096    @Override
097    public int[] getAcceptableTokens() {
098        return new int[] {
099            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}