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 -> !isInSwitchRule(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 -> !isInSwitchRule(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 switch member (case or default statement) in a switch rule or
371     * case group is on a single-line.
372     *
373     * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
374     *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
375     * @return true if current switch member is single-line statement.
376     */
377    private static boolean isSingleLineSwitchMember(DetailAST statement) {
378        final boolean result;
379        if (isInSwitchRule(statement)) {
380            result = isSingleLineSwitchRule(statement);
381        }
382        else {
383            result = isSingleLineCaseGroup(statement);
384        }
385        return result;
386    }
387
388    /**
389     * Checks if switch member in case group (case or default statement)
390     * is single-line statement, e.g.:
391     *
392     * <p>
393     * {@code
394     * case 1: System.out.println("case one"); break;
395     * case 2: System.out.println("case two"); break;
396     * case 3: ;
397     * default: System.out.println("default"); break;
398     * }
399     * </p>
400     *
401     *
402     * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
403     *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
404     * @return true if current switch member is single-line statement.
405     */
406    private static boolean isSingleLineCaseGroup(DetailAST ast) {
407        return Optional.of(ast)
408            .map(DetailAST::getNextSibling)
409            .map(DetailAST::getLastChild)
410            .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
411            .orElse(Boolean.TRUE);
412    }
413
414    /**
415     * Checks if switch member in switch rule (case or default statement) is
416     * single-line statement, e.g.:
417     *
418     * <p>
419     * {@code
420     * case 1 -> System.out.println("case one");
421     * case 2 -> System.out.println("case two");
422     * default -> System.out.println("default");
423     * }
424     * </p>
425     *
426     * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
427     *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
428     * @return true if current switch label is single-line statement.
429     */
430    private static boolean isSingleLineSwitchRule(DetailAST ast) {
431        final DetailAST lastSibling = ast.getParent().getLastChild();
432        return TokenUtil.areOnSameLine(ast, lastSibling);
433    }
434
435    /**
436     * Checks if current else statement is single-line statement, e.g.:
437     *
438     * <p>
439     * {@code
440     * else doSomeStuff();
441     * }
442     * </p>
443     *
444     * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
445     * @return true if current else statement is single-line statement.
446     */
447    private static boolean isSingleLineElse(DetailAST literalElse) {
448        final DetailAST block = literalElse.getFirstChild();
449        return TokenUtil.areOnSameLine(literalElse, block);
450    }
451
452}