View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.blocks;
21  
22  import java.util.Optional;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
29  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
30  
31  /**
32   * <div>
33   * Checks for braces around code blocks.
34   * </div>
35   *
36   * <p>
37   * Attention: The break in case blocks is not counted to allow compact view.
38   * </p>
39   * <ul>
40   * <li>
41   * Property {@code allowEmptyLoopBody} - Allow loops with empty bodies.
42   * Type is {@code boolean}.
43   * Default value is {@code false}.
44   * </li>
45   * <li>
46   * Property {@code allowSingleLineStatement} - Allow single-line statements without braces.
47   * Type is {@code boolean}.
48   * Default value is {@code false}.
49   * </li>
50   * <li>
51   * Property {@code tokens} - tokens to check
52   * Type is {@code java.lang.String[]}.
53   * Validation type is {@code tokenSet}.
54   * Default value is:
55   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
56   * LITERAL_DO</a>,
57   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE">
58   * LITERAL_ELSE</a>,
59   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
60   * LITERAL_FOR</a>,
61   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
62   * LITERAL_IF</a>,
63   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
64   * LITERAL_WHILE</a>.
65   * </li>
66   * </ul>
67   *
68   * <p>
69   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
70   * </p>
71   *
72   * <p>
73   * Violation Message Keys:
74   * </p>
75   * <ul>
76   * <li>
77   * {@code needBraces}
78   * </li>
79   * </ul>
80   *
81   * @since 3.0
82   */
83  @StatelessCheck
84  public class NeedBracesCheck extends AbstractCheck {
85  
86      /**
87       * A key is pointing to the warning message text in "messages.properties"
88       * file.
89       */
90      public static final String MSG_KEY_NEED_BRACES = "needBraces";
91  
92      /**
93       * Allow single-line statements without braces.
94       */
95      private boolean allowSingleLineStatement;
96  
97      /**
98       * Allow loops with empty bodies.
99       */
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         return switch (ast.getType()) {
179             case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE -> !isEmptyLoopBodyAllowed(ast);
180             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> hasUnbracedStatements(ast);
181             case TokenTypes.LITERAL_ELSE -> ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
182             case TokenTypes.LAMBDA -> !isInSwitchRule(ast);
183             default -> true;
184         };
185     }
186 
187     /**
188      * Checks if current loop has empty body and can be skipped by this check.
189      *
190      * @param ast for, while statements.
191      * @return true if current loop can be skipped by check.
192      */
193     private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
194         return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
195     }
196 
197     /**
198      * Checks if switch member (case, default statements) has statements without curly braces.
199      *
200      * @param ast case, default statements.
201      * @return true if switch member has unbraced statements, false otherwise.
202      */
203     private static boolean hasUnbracedStatements(DetailAST ast) {
204         final DetailAST nextSibling = ast.getNextSibling();
205         boolean result = false;
206 
207         if (isInSwitchRule(ast)) {
208             final DetailAST parent = ast.getParent();
209             result = parent.getLastChild().getType() != TokenTypes.SLIST;
210         }
211         else if (nextSibling != null
212             && nextSibling.getType() == TokenTypes.SLIST
213             && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
214             result = true;
215         }
216         return result;
217     }
218 
219     /**
220      * Checks if current statement can be skipped by "need braces" warning.
221      *
222      * @param statement if, for, while, do-while, lambda, else, case, default statements.
223      * @return true if current statement can be skipped by Check.
224      */
225     private boolean isSkipStatement(DetailAST statement) {
226         return allowSingleLineStatement && isSingleLineStatement(statement);
227     }
228 
229     /**
230      * Checks if current statement is single-line statement, e.g.:
231      *
232      * <p>
233      * {@code
234      * if (obj.isValid()) return true;
235      * }
236      * </p>
237      *
238      * <p>
239      * {@code
240      * while (obj.isValid()) return true;
241      * }
242      * </p>
243      *
244      * @param statement if, for, while, do-while, lambda, else, case, default statements.
245      * @return true if current statement is single-line statement.
246      */
247     private static boolean isSingleLineStatement(DetailAST statement) {
248 
249         return switch (statement.getType()) {
250             case TokenTypes.LITERAL_IF -> isSingleLineIf(statement);
251             case TokenTypes.LITERAL_FOR -> isSingleLineFor(statement);
252             case TokenTypes.LITERAL_DO -> isSingleLineDoWhile(statement);
253             case TokenTypes.LITERAL_WHILE -> isSingleLineWhile(statement);
254             case TokenTypes.LAMBDA -> !isInSwitchRule(statement)
255                     && isSingleLineLambda(statement);
256             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT ->
257                 isSingleLineSwitchMember(statement);
258             default -> isSingleLineElse(statement);
259         };
260     }
261 
262     /**
263      * Checks if current while statement is single-line statement, e.g.:
264      *
265      * <p>
266      * {@code
267      * while (obj.isValid()) return true;
268      * }
269      * </p>
270      *
271      * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
272      * @return true if current while statement is single-line statement.
273      */
274     private static boolean isSingleLineWhile(DetailAST literalWhile) {
275         boolean result = false;
276         if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
277             final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
278             result = TokenUtil.areOnSameLine(literalWhile, block);
279         }
280         return result;
281     }
282 
283     /**
284      * Checks if current do-while statement is single-line statement, e.g.:
285      *
286      * <p>
287      * {@code
288      * do this.notify(); while (o != null);
289      * }
290      * </p>
291      *
292      * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
293      * @return true if current do-while statement is single-line statement.
294      */
295     private static boolean isSingleLineDoWhile(DetailAST literalDo) {
296         boolean result = false;
297         if (literalDo.getParent().getType() == TokenTypes.SLIST) {
298             final DetailAST block = literalDo.getFirstChild();
299             result = TokenUtil.areOnSameLine(block, literalDo);
300         }
301         return result;
302     }
303 
304     /**
305      * Checks if current for statement is single-line statement, e.g.:
306      *
307      * <p>
308      * {@code
309      * for (int i = 0; ; ) this.notify();
310      * }
311      * </p>
312      *
313      * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
314      * @return true if current for statement is single-line statement.
315      */
316     private static boolean isSingleLineFor(DetailAST literalFor) {
317         boolean result = false;
318         if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
319             result = true;
320         }
321         else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
322             result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
323         }
324         return result;
325     }
326 
327     /**
328      * Checks if current if statement is single-line statement, e.g.:
329      *
330      * <p>
331      * {@code
332      * if (obj.isValid()) return true;
333      * }
334      * </p>
335      *
336      * @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
337      * @return true if current if statement is single-line statement.
338      */
339     private static boolean isSingleLineIf(DetailAST literalIf) {
340         boolean result = false;
341         if (literalIf.getParent().getType() == TokenTypes.SLIST) {
342             final DetailAST literalIfLastChild = literalIf.getLastChild();
343             final DetailAST block;
344             if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
345                 block = literalIfLastChild.getPreviousSibling();
346             }
347             else {
348                 block = literalIfLastChild;
349             }
350             final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
351             result = TokenUtil.areOnSameLine(ifCondition, block);
352         }
353         return result;
354     }
355 
356     /**
357      * Checks if current lambda statement is single-line statement, e.g.:
358      *
359      * <p>
360      * {@code
361      * Runnable r = () -> System.out.println("Hello, world!");
362      * }
363      * </p>
364      *
365      * @param lambda {@link TokenTypes#LAMBDA lambda statement}.
366      * @return true if current lambda statement is single-line statement.
367      */
368     private static boolean isSingleLineLambda(DetailAST lambda) {
369         final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
370         return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
371     }
372 
373     /**
374      * Looks for the last token in lambda.
375      *
376      * @param lambda token to check.
377      * @return last token in lambda
378      */
379     private static DetailAST getLastLambdaToken(DetailAST lambda) {
380         DetailAST node = lambda;
381         do {
382             node = node.getLastChild();
383         } while (node.getLastChild() != null);
384         return node;
385     }
386 
387     /**
388      * Checks if current ast's parent is a switch rule, e.g.:
389      *
390      * <p>
391      * {@code
392      * case 1 ->  monthString = "January";
393      * }
394      * </p>
395      *
396      * @param ast the ast to check.
397      * @return true if current ast belongs to a switch rule.
398      */
399     private static boolean isInSwitchRule(DetailAST ast) {
400         return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
401     }
402 
403     /**
404      * Checks if switch member (case or default statement) in a switch rule or
405      * case group is on a single-line.
406      *
407      * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
408      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
409      * @return true if current switch member is single-line statement.
410      */
411     private static boolean isSingleLineSwitchMember(DetailAST statement) {
412         final boolean result;
413         if (isInSwitchRule(statement)) {
414             result = isSingleLineSwitchRule(statement);
415         }
416         else {
417             result = isSingleLineCaseGroup(statement);
418         }
419         return result;
420     }
421 
422     /**
423      * Checks if switch member in case group (case or default statement)
424      * is single-line statement, e.g.:
425      *
426      * <p>
427      * {@code
428      * case 1: System.out.println("case one"); break;
429      * case 2: System.out.println("case two"); break;
430      * case 3: ;
431      * default: System.out.println("default"); break;
432      * }
433      * </p>
434      *
435      *
436      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
437      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
438      * @return true if current switch member is single-line statement.
439      */
440     private static boolean isSingleLineCaseGroup(DetailAST ast) {
441         return Optional.of(ast)
442             .map(DetailAST::getNextSibling)
443             .map(DetailAST::getLastChild)
444             .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
445             .orElse(Boolean.TRUE);
446     }
447 
448     /**
449      * Checks if switch member in switch rule (case or default statement) is
450      * single-line statement, e.g.:
451      *
452      * <p>
453      * {@code
454      * case 1 -> System.out.println("case one");
455      * case 2 -> System.out.println("case two");
456      * default -> System.out.println("default");
457      * }
458      * </p>
459      *
460      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
461      *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
462      * @return true if current switch label is single-line statement.
463      */
464     private static boolean isSingleLineSwitchRule(DetailAST ast) {
465         final DetailAST lastSibling = ast.getParent().getLastChild();
466         return TokenUtil.areOnSameLine(ast, lastSibling);
467     }
468 
469     /**
470      * Checks if current else statement is single-line statement, e.g.:
471      *
472      * <p>
473      * {@code
474      * else doSomeStuff();
475      * }
476      * </p>
477      *
478      * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
479      * @return true if current else statement is single-line statement.
480      */
481     private static boolean isSingleLineElse(DetailAST literalElse) {
482         final DetailAST block = literalElse.getFirstChild();
483         return TokenUtil.areOnSameLine(literalElse, block);
484     }
485 
486 }