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