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.Optional;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
030
031/**
032 * <p>
033 * Checks for braces around code blocks.
034 * </p>
035 * <ul>
036 * <li>
037 * Property {@code allowEmptyLoopBody} - Allow loops with empty bodies.
038 * Type is {@code boolean}.
039 * Default value is {@code false}.
040 * </li>
041 * <li>
042 * Property {@code allowSingleLineStatement} - Allow single-line statements without braces.
043 * Type is {@code boolean}.
044 * Default value is {@code false}.
045 * </li>
046 * <li>
047 * Property {@code tokens} - tokens to check
048 * Type is {@code java.lang.String[]}.
049 * Validation type is {@code tokenSet}.
050 * Default value is:
051 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
052 * LITERAL_DO</a>,
053 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE">
054 * LITERAL_ELSE</a>,
055 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
056 * LITERAL_FOR</a>,
057 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
058 * LITERAL_IF</a>,
059 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
060 * LITERAL_WHILE</a>.
061 * </li>
062 * </ul>
063 * <p>
064 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
065 * </p>
066 * <p>
067 * Violation Message Keys:
068 * </p>
069 * <ul>
070 * <li>
071 * {@code needBraces}
072 * </li>
073 * </ul>
074 *
075 * @since 3.0
076 */
077@StatelessCheck
078public class NeedBracesCheck extends AbstractCheck {
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_KEY_NEED_BRACES = "needBraces";
085
086    /**
087     * Allow single-line statements without braces.
088     */
089    private boolean allowSingleLineStatement;
090
091    /**
092     * Allow loops with empty bodies.
093     */
094    private boolean allowEmptyLoopBody;
095
096    /**
097     * Setter to allow single-line statements without braces.
098     *
099     * @param allowSingleLineStatement Check's option for skipping single-line statements
100     * @since 6.5
101     */
102    public void setAllowSingleLineStatement(boolean allowSingleLineStatement) {
103        this.allowSingleLineStatement = allowSingleLineStatement;
104    }
105
106    /**
107     * Setter to allow loops with empty bodies.
108     *
109     * @param allowEmptyLoopBody Check's option for allowing loops with empty body.
110     * @since 6.12.1
111     */
112    public void setAllowEmptyLoopBody(boolean allowEmptyLoopBody) {
113        this.allowEmptyLoopBody = allowEmptyLoopBody;
114    }
115
116    @Override
117    public int[] getDefaultTokens() {
118        return new int[] {
119            TokenTypes.LITERAL_DO,
120            TokenTypes.LITERAL_ELSE,
121            TokenTypes.LITERAL_FOR,
122            TokenTypes.LITERAL_IF,
123            TokenTypes.LITERAL_WHILE,
124        };
125    }
126
127    @Override
128    public int[] getAcceptableTokens() {
129        return new int[] {
130            TokenTypes.LITERAL_DO,
131            TokenTypes.LITERAL_ELSE,
132            TokenTypes.LITERAL_FOR,
133            TokenTypes.LITERAL_IF,
134            TokenTypes.LITERAL_WHILE,
135            TokenTypes.LITERAL_CASE,
136            TokenTypes.LITERAL_DEFAULT,
137            TokenTypes.LAMBDA,
138        };
139    }
140
141    @Override
142    public int[] getRequiredTokens() {
143        return CommonUtil.EMPTY_INT_ARRAY;
144    }
145
146    @Override
147    public void visitToken(DetailAST ast) {
148        final boolean hasNoSlist = ast.findFirstToken(TokenTypes.SLIST) == null;
149        if (hasNoSlist && !isSkipStatement(ast) && isBracesNeeded(ast)) {
150            log(ast, MSG_KEY_NEED_BRACES, ast.getText());
151        }
152    }
153
154    /**
155     * Checks if token needs braces.
156     * Some tokens have additional conditions:
157     * <ul>
158     *     <li>{@link TokenTypes#LITERAL_FOR}</li>
159     *     <li>{@link TokenTypes#LITERAL_WHILE}</li>
160     *     <li>{@link TokenTypes#LITERAL_CASE}</li>
161     *     <li>{@link TokenTypes#LITERAL_DEFAULT}</li>
162     *     <li>{@link TokenTypes#LITERAL_ELSE}</li>
163     *     <li>{@link TokenTypes#LAMBDA}</li>
164     * </ul>
165     * For all others default value {@code true} is returned.
166     *
167     * @param ast token to check
168     * @return result of additional checks for specific token types,
169     *     {@code true} if there is no additional checks for token
170     */
171    private boolean isBracesNeeded(DetailAST ast) {
172        final boolean result;
173        switch (ast.getType()) {
174            case TokenTypes.LITERAL_FOR:
175            case TokenTypes.LITERAL_WHILE:
176                result = !isEmptyLoopBodyAllowed(ast);
177                break;
178            case TokenTypes.LITERAL_CASE:
179            case TokenTypes.LITERAL_DEFAULT:
180                result = hasUnbracedStatements(ast)
181                    && !isSwitchLabeledExpression(ast);
182                break;
183            case TokenTypes.LITERAL_ELSE:
184                result = ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
185                break;
186            case TokenTypes.LAMBDA:
187                result = !isInSwitchRule(ast);
188                break;
189            default:
190                result = true;
191                break;
192        }
193        return result;
194    }
195
196    /**
197     * Checks if current loop has empty body and can be skipped by this check.
198     *
199     * @param ast for, while statements.
200     * @return true if current loop can be skipped by check.
201     */
202    private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
203        return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
204    }
205
206    /**
207     * Checks if switch member (case, default statements) has statements without curly braces.
208     *
209     * @param ast case, default statements.
210     * @return true if switch member has unbraced statements, false otherwise.
211     */
212    private static boolean hasUnbracedStatements(DetailAST ast) {
213        final DetailAST nextSibling = ast.getNextSibling();
214        boolean result = false;
215
216        if (isInSwitchRule(ast)) {
217            final DetailAST parent = ast.getParent();
218            result = parent.getLastChild().getType() != TokenTypes.SLIST;
219        }
220        else if (nextSibling != null
221            && nextSibling.getType() == TokenTypes.SLIST
222            && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
223            result = true;
224        }
225        return result;
226    }
227
228    /**
229     * Checks if current statement can be skipped by "need braces" warning.
230     *
231     * @param statement if, for, while, do-while, lambda, else, case, default statements.
232     * @return true if current statement can be skipped by Check.
233     */
234    private boolean isSkipStatement(DetailAST statement) {
235        return allowSingleLineStatement && isSingleLineStatement(statement);
236    }
237
238    /**
239     * Checks if current statement is single-line statement, e.g.:
240     * <p>
241     * {@code
242     * if (obj.isValid()) return true;
243     * }
244     * </p>
245     * <p>
246     * {@code
247     * while (obj.isValid()) return true;
248     * }
249     * </p>
250     *
251     * @param statement if, for, while, do-while, lambda, else, case, default statements.
252     * @return true if current statement is single-line statement.
253     */
254    private static boolean isSingleLineStatement(DetailAST statement) {
255        final boolean result;
256
257        switch (statement.getType()) {
258            case TokenTypes.LITERAL_IF:
259                result = isSingleLineIf(statement);
260                break;
261            case TokenTypes.LITERAL_FOR:
262                result = isSingleLineFor(statement);
263                break;
264            case TokenTypes.LITERAL_DO:
265                result = isSingleLineDoWhile(statement);
266                break;
267            case TokenTypes.LITERAL_WHILE:
268                result = isSingleLineWhile(statement);
269                break;
270            case TokenTypes.LAMBDA:
271                result = !isInSwitchRule(statement)
272                    && isSingleLineLambda(statement);
273                break;
274            case TokenTypes.LITERAL_CASE:
275            case TokenTypes.LITERAL_DEFAULT:
276                result = isSingleLineSwitchMember(statement);
277                break;
278            default:
279                result = isSingleLineElse(statement);
280                break;
281        }
282
283        return result;
284    }
285
286    /**
287     * Checks if current while statement is single-line statement, e.g.:
288     * <p>
289     * {@code
290     * while (obj.isValid()) return true;
291     * }
292     * </p>
293     *
294     * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
295     * @return true if current while statement is single-line statement.
296     */
297    private static boolean isSingleLineWhile(DetailAST literalWhile) {
298        boolean result = false;
299        if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
300            final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
301            result = TokenUtil.areOnSameLine(literalWhile, block);
302        }
303        return result;
304    }
305
306    /**
307     * Checks if current do-while statement is single-line statement, e.g.:
308     * <p>
309     * {@code
310     * do this.notify(); while (o != null);
311     * }
312     * </p>
313     *
314     * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
315     * @return true if current do-while statement is single-line statement.
316     */
317    private static boolean isSingleLineDoWhile(DetailAST literalDo) {
318        boolean result = false;
319        if (literalDo.getParent().getType() == TokenTypes.SLIST) {
320            final DetailAST block = literalDo.getFirstChild();
321            result = TokenUtil.areOnSameLine(block, literalDo);
322        }
323        return result;
324    }
325
326    /**
327     * Checks if current for statement is single-line statement, e.g.:
328     * <p>
329     * {@code
330     * for (int i = 0; ; ) this.notify();
331     * }
332     * </p>
333     *
334     * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
335     * @return true if current for statement is single-line statement.
336     */
337    private static boolean isSingleLineFor(DetailAST literalFor) {
338        boolean result = false;
339        if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
340            result = true;
341        }
342        else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
343            result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
344        }
345        return result;
346    }
347
348    /**
349     * Checks if current if statement is single-line statement, e.g.:
350     * <p>
351     * {@code
352     * if (obj.isValid()) return true;
353     * }
354     * </p>
355     *
356     * @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
357     * @return true if current if statement is single-line statement.
358     */
359    private static boolean isSingleLineIf(DetailAST literalIf) {
360        boolean result = false;
361        if (literalIf.getParent().getType() == TokenTypes.SLIST) {
362            final DetailAST literalIfLastChild = literalIf.getLastChild();
363            final DetailAST block;
364            if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
365                block = literalIfLastChild.getPreviousSibling();
366            }
367            else {
368                block = literalIfLastChild;
369            }
370            final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
371            result = TokenUtil.areOnSameLine(ifCondition, block);
372        }
373        return result;
374    }
375
376    /**
377     * Checks if current lambda statement is single-line statement, e.g.:
378     * <p>
379     * {@code
380     * Runnable r = () -> System.out.println("Hello, world!");
381     * }
382     * </p>
383     *
384     * @param lambda {@link TokenTypes#LAMBDA lambda statement}.
385     * @return true if current lambda statement is single-line statement.
386     */
387    private static boolean isSingleLineLambda(DetailAST lambda) {
388        final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
389        return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
390    }
391
392    /**
393     * Looks for the last token in lambda.
394     *
395     * @param lambda token to check.
396     * @return last token in lambda
397     */
398    private static DetailAST getLastLambdaToken(DetailAST lambda) {
399        DetailAST node = lambda;
400        do {
401            node = node.getLastChild();
402        } while (node.getLastChild() != null);
403        return node;
404    }
405
406    /**
407     * Checks if current ast's parent is a switch rule, e.g.:
408     * <p>
409     * {@code
410     * case 1 ->  monthString = "January";
411     * }
412     * </p>
413     *
414     * @param ast the ast to check.
415     * @return true if current ast belongs to a switch rule.
416     */
417    private static boolean isInSwitchRule(DetailAST ast) {
418        return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
419    }
420
421    /**
422     * Checks if current expression is a switch labeled expression. If so,
423     * braces are not allowed e.g.:
424     * <p>
425     * {@code
426     * case 1 -> 4;
427     * }
428     * </p>
429     *
430     * @param ast the ast to check
431     * @return true if current expression is a switch labeled expression.
432     */
433    private static boolean isSwitchLabeledExpression(DetailAST ast) {
434        final DetailAST parent = ast.getParent();
435        return switchRuleHasSingleExpression(parent);
436    }
437
438    /**
439     * Checks if current switch labeled expression contains only a single expression.
440     *
441     * @param switchRule {@link TokenTypes#SWITCH_RULE}.
442     * @return true if current switch rule has a single expression.
443     */
444    private static boolean switchRuleHasSingleExpression(DetailAST switchRule) {
445        final DetailAST possibleExpression = switchRule.findFirstToken(TokenTypes.EXPR);
446        return possibleExpression != null
447                && possibleExpression.getFirstChild().getFirstChild() == null;
448    }
449
450    /**
451     * Checks if switch member (case or default statement) in a switch rule or
452     * case group is on a single-line.
453     *
454     * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
455     *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
456     * @return true if current switch member is single-line statement.
457     */
458    private static boolean isSingleLineSwitchMember(DetailAST statement) {
459        final boolean result;
460        if (isInSwitchRule(statement)) {
461            result = isSingleLineSwitchRule(statement);
462        }
463        else {
464            result = isSingleLineCaseGroup(statement);
465        }
466        return result;
467    }
468
469    /**
470     * Checks if switch member in case group (case or default statement)
471     * is single-line statement, e.g.:
472     * <p>
473     * {@code
474     * case 1: System.out.println("case one"); break;
475     * case 2: System.out.println("case two"); break;
476     * case 3: ;
477     * default: System.out.println("default"); break;
478     * }
479     * </p>
480     *
481     *
482     * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
483     *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
484     * @return true if current switch member is single-line statement.
485     */
486    private static boolean isSingleLineCaseGroup(DetailAST ast) {
487        return Optional.of(ast)
488            .map(DetailAST::getNextSibling)
489            .map(DetailAST::getLastChild)
490            .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
491            .orElse(Boolean.TRUE);
492    }
493
494    /**
495     * Checks if switch member in switch rule (case or default statement) is
496     * single-line statement, e.g.:
497     * <p>
498     * {@code
499     * case 1 -> System.out.println("case one");
500     * case 2 -> System.out.println("case two");
501     * default -> System.out.println("default");
502     * }
503     * </p>
504     *
505     * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
506     *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
507     * @return true if current switch label is single-line statement.
508     */
509    private static boolean isSingleLineSwitchRule(DetailAST ast) {
510        final DetailAST lastSibling = ast.getParent().getLastChild();
511        return TokenUtil.areOnSameLine(ast, lastSibling);
512    }
513
514    /**
515     * Checks if current else statement is single-line statement, e.g.:
516     * <p>
517     * {@code
518     * else doSomeStuff();
519     * }
520     * </p>
521     *
522     * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
523     * @return true if current else statement is single-line statement.
524     */
525    private static boolean isSingleLineElse(DetailAST literalElse) {
526        final DetailAST block = literalElse.getFirstChild();
527        return TokenUtil.areOnSameLine(literalElse, block);
528    }
529
530}