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