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