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