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