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