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