View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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   *
40   * @since 3.0
41   */
42  @StatelessCheck
43  public class NeedBracesCheck extends AbstractCheck {
44  
45      /**
46       * A key is pointing to the warning message text in "messages.properties"
47       * file.
48       */
49      public static final String MSG_KEY_NEED_BRACES = "needBraces";
50  
51      /**
52       * Allow single-line statements without braces.
53       */
54      private boolean allowSingleLineStatement;
55  
56      /**
57       * Allow loops with empty bodies.
58       */
59      private boolean allowEmptyLoopBody;
60  
61      /**
62       * Creates a new {@code NeedBracesCheck} instance.
63       */
64      public NeedBracesCheck() {
65          // no code by default
66      }
67  
68      /**
69       * Setter to allow single-line statements without braces.
70       *
71       * @param allowSingleLineStatement Check's option for skipping single-line statements
72       * @since 6.5
73       */
74      public void setAllowSingleLineStatement(boolean allowSingleLineStatement) {
75          this.allowSingleLineStatement = allowSingleLineStatement;
76      }
77  
78      /**
79       * Setter to allow loops with empty bodies.
80       *
81       * @param allowEmptyLoopBody Check's option for allowing loops with empty body.
82       * @since 6.12.1
83       */
84      public void setAllowEmptyLoopBody(boolean allowEmptyLoopBody) {
85          this.allowEmptyLoopBody = allowEmptyLoopBody;
86      }
87  
88      @Override
89      public int[] getDefaultTokens() {
90          return new int[] {
91              TokenTypes.LITERAL_DO,
92              TokenTypes.LITERAL_ELSE,
93              TokenTypes.LITERAL_FOR,
94              TokenTypes.LITERAL_IF,
95              TokenTypes.LITERAL_WHILE,
96          };
97      }
98  
99      @Override
100     public int[] getAcceptableTokens() {
101         return new int[] {
102             TokenTypes.LITERAL_DO,
103             TokenTypes.LITERAL_ELSE,
104             TokenTypes.LITERAL_FOR,
105             TokenTypes.LITERAL_IF,
106             TokenTypes.LITERAL_WHILE,
107             TokenTypes.LITERAL_CASE,
108             TokenTypes.LITERAL_DEFAULT,
109             TokenTypes.LAMBDA,
110         };
111     }
112 
113     @Override
114     public int[] getRequiredTokens() {
115         return CommonUtil.EMPTY_INT_ARRAY;
116     }
117 
118     @Override
119     public void visitToken(DetailAST ast) {
120         final boolean hasNoSlist = ast.findFirstToken(TokenTypes.SLIST) == null;
121         if (hasNoSlist && !isSkipStatement(ast) && isBracesNeeded(ast)) {
122             log(ast, MSG_KEY_NEED_BRACES, ast.getText());
123         }
124     }
125 
126     /**
127      * Checks if token needs braces.
128      * Some tokens have additional conditions:
129      * <ul>
130      *     <li>{@link TokenTypes#LITERAL_FOR}</li>
131      *     <li>{@link TokenTypes#LITERAL_WHILE}</li>
132      *     <li>{@link TokenTypes#LITERAL_CASE}</li>
133      *     <li>{@link TokenTypes#LITERAL_DEFAULT}</li>
134      *     <li>{@link TokenTypes#LITERAL_ELSE}</li>
135      *     <li>{@link TokenTypes#LAMBDA}</li>
136      * </ul>
137      * For all others default value {@code true} is returned.
138      *
139      * @param ast token to check
140      * @return result of additional checks for specific token types,
141      *     {@code true} if there is no additional checks for token
142      */
143     private boolean isBracesNeeded(DetailAST ast) {
144         return switch (ast.getType()) {
145             case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE -> !isEmptyLoopBodyAllowed(ast);
146             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> hasUnbracedStatements(ast);
147             case TokenTypes.LITERAL_ELSE -> ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
148             case TokenTypes.LAMBDA -> !isSwitchRuleLambda(ast);
149             default -> true;
150         };
151     }
152 
153     /**
154      * Checks if current loop has empty body and can be skipped by this check.
155      *
156      * @param ast for, while statements.
157      * @return true if current loop can be skipped by check.
158      */
159     private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
160         return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
161     }
162 
163     /**
164      * Checks if switch member (case, default statements) has statements without curly braces.
165      *
166      * @param ast case, default statements.
167      * @return true if switch member has unbraced statements, false otherwise.
168      */
169     private static boolean hasUnbracedStatements(DetailAST ast) {
170         final DetailAST nextSibling = ast.getNextSibling();
171         boolean result = false;
172 
173         if (isInSwitchRule(ast)) {
174             final DetailAST parent = ast.getParent();
175             result = parent.getLastChild().getType() != TokenTypes.SLIST;
176         }
177         else if (nextSibling != null
178             && nextSibling.getType() == TokenTypes.SLIST
179             && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
180             result = true;
181         }
182         return result;
183     }
184 
185     /**
186      * Checks if current statement can be skipped by "need braces" warning.
187      *
188      * @param statement if, for, while, do-while, lambda, else, case, default statements.
189      * @return true if current statement can be skipped by Check.
190      */
191     private boolean isSkipStatement(DetailAST statement) {
192         return allowSingleLineStatement && isSingleLineStatement(statement);
193     }
194 
195     /**
196      * Checks if current statement is single-line statement, e.g.:
197      *
198      * <p>
199      * {@code
200      * if (obj.isValid()) return true;
201      * }
202      * </p>
203      *
204      * <p>
205      * {@code
206      * while (obj.isValid()) return true;
207      * }
208      * </p>
209      *
210      * @param statement if, for, while, do-while, lambda, else, case, default statements.
211      * @return true if current statement is single-line statement.
212      */
213     private static boolean isSingleLineStatement(DetailAST statement) {
214 
215         return switch (statement.getType()) {
216             case TokenTypes.LITERAL_IF -> isSingleLineIf(statement);
217             case TokenTypes.LITERAL_FOR -> isSingleLineFor(statement);
218             case TokenTypes.LITERAL_DO -> isSingleLineDoWhile(statement);
219             case TokenTypes.LITERAL_WHILE -> isSingleLineWhile(statement);
220             case TokenTypes.LAMBDA -> !isSwitchRuleLambda(statement)
221                     && isSingleLineLambda(statement);
222             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT ->
223                 isSingleLineSwitchMember(statement);
224             default -> isSingleLineElse(statement);
225         };
226     }
227 
228     /**
229      * Checks if current while statement is single-line statement, e.g.:
230      *
231      * <p>
232      * {@code
233      * while (obj.isValid()) return true;
234      * }
235      * </p>
236      *
237      * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
238      * @return true if current while statement is single-line statement.
239      */
240     private static boolean isSingleLineWhile(DetailAST literalWhile) {
241         boolean result = false;
242         if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
243             final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
244             result = TokenUtil.areOnSameLine(literalWhile, block);
245         }
246         return result;
247     }
248 
249     /**
250      * Checks if current do-while statement is single-line statement, e.g.:
251      *
252      * <p>
253      * {@code
254      * do this.notify(); while (o != null);
255      * }
256      * </p>
257      *
258      * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
259      * @return true if current do-while statement is single-line statement.
260      */
261     private static boolean isSingleLineDoWhile(DetailAST literalDo) {
262         boolean result = false;
263         if (literalDo.getParent().getType() == TokenTypes.SLIST) {
264             final DetailAST block = literalDo.getFirstChild();
265             result = TokenUtil.areOnSameLine(block, literalDo);
266         }
267         return result;
268     }
269 
270     /**
271      * Checks if current for statement is single-line statement, e.g.:
272      *
273      * <p>
274      * {@code
275      * for (int i = 0; ; ) this.notify();
276      * }
277      * </p>
278      *
279      * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
280      * @return true if current for statement is single-line statement.
281      */
282     private static boolean isSingleLineFor(DetailAST literalFor) {
283         boolean result = false;
284         if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
285             result = true;
286         }
287         else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
288             result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
289         }
290         return result;
291     }
292 
293     /**
294      * Checks if current if statement is single-line statement, e.g.:
295      *
296      * <p>
297      * {@code
298      * if (obj.isValid()) return true;
299      * }
300      * </p>
301      *
302      * @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
303      * @return true if current if statement is single-line statement.
304      */
305     private static boolean isSingleLineIf(DetailAST literalIf) {
306         boolean result = false;
307         if (literalIf.getParent().getType() == TokenTypes.SLIST) {
308             final DetailAST literalIfLastChild = literalIf.getLastChild();
309             final DetailAST block;
310             if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
311                 block = literalIfLastChild.getPreviousSibling();
312             }
313             else {
314                 block = literalIfLastChild;
315             }
316             final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
317             result = TokenUtil.areOnSameLine(ifCondition, block);
318         }
319         return result;
320     }
321 
322     /**
323      * Checks if current lambda statement is single-line statement, e.g.:
324      *
325      * <p>
326      * {@code
327      * Runnable r = () -> System.out.println("Hello, world!");
328      * }
329      * </p>
330      *
331      * @param lambda {@link TokenTypes#LAMBDA lambda statement}.
332      * @return true if current lambda statement is single-line statement.
333      */
334     private static boolean isSingleLineLambda(DetailAST lambda) {
335         final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
336         return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
337     }
338 
339     /**
340      * Looks for the last token in lambda.
341      *
342      * @param lambda token to check.
343      * @return last token in lambda
344      */
345     private static DetailAST getLastLambdaToken(DetailAST lambda) {
346         DetailAST node = lambda;
347         do {
348             node = node.getLastChild();
349         } while (node.getLastChild() != null);
350         return node;
351     }
352 
353     /**
354      * Checks if current ast's parent is a switch rule, e.g.:
355      *
356      * <p>
357      * {@code
358      * case 1 ->  monthString = "January";
359      * }
360      * </p>
361      *
362      * @param ast the ast to check.
363      * @return true if current ast belongs to a switch rule.
364      */
365     private static boolean isInSwitchRule(DetailAST ast) {
366         return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
367     }
368 
369     /**
370      * Checks if the provided LAMBDA node is a switch rule lambda.
371      *
372      * @param lambda the ast to check.
373      * @return true if lambda is a switch rule lambda.
374      */
375     private static boolean isSwitchRuleLambda(DetailAST lambda) {
376         return !lambda.hasChildren();
377     }
378 
379     /**
380      * Checks if switch member (case or default statement) in a switch rule or
381      * case group is on a single-line.
382      *
383      * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
384      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
385      * @return true if current switch member is single-line statement.
386      */
387     private static boolean isSingleLineSwitchMember(DetailAST statement) {
388         final boolean result;
389         if (isInSwitchRule(statement)) {
390             result = isSingleLineSwitchRule(statement);
391         }
392         else {
393             result = isSingleLineCaseGroup(statement);
394         }
395         return result;
396     }
397 
398     /**
399      * Checks if switch member in case group (case or default statement)
400      * is single-line statement, e.g.:
401      *
402      * <p>
403      * {@code
404      * case 1: System.out.println("case one"); break;
405      * case 2: System.out.println("case two"); break;
406      * case 3: ;
407      * default: System.out.println("default"); break;
408      * }
409      * </p>
410      *
411      *
412      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
413      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
414      * @return true if current switch member is single-line statement.
415      */
416     private static boolean isSingleLineCaseGroup(DetailAST ast) {
417         return Optional.of(ast)
418             .map(DetailAST::getNextSibling)
419             .map(DetailAST::getLastChild)
420             .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
421             .orElse(Boolean.TRUE);
422     }
423 
424     /**
425      * Checks if switch member in switch rule (case or default statement) is
426      * single-line statement, e.g.:
427      *
428      * <p>
429      * {@code
430      * case 1 -> System.out.println("case one");
431      * case 2 -> System.out.println("case two");
432      * default -> System.out.println("default");
433      * }
434      * </p>
435      *
436      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
437      *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
438      * @return true if current switch label is single-line statement.
439      */
440     private static boolean isSingleLineSwitchRule(DetailAST ast) {
441         final DetailAST lastSibling = ast.getParent().getLastChild();
442         return TokenUtil.areOnSameLine(ast, lastSibling);
443     }
444 
445     /**
446      * Checks if current else statement is single-line statement, e.g.:
447      *
448      * <p>
449      * {@code
450      * else doSomeStuff();
451      * }
452      * </p>
453      *
454      * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
455      * @return true if current else statement is single-line statement.
456      */
457     private static boolean isSingleLineElse(DetailAST literalElse) {
458         final DetailAST block = literalElse.getFirstChild();
459         return TokenUtil.areOnSameLine(literalElse, block);
460     }
461 
462 }