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