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