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