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 -> !isInSwitchRule(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 -> !isInSwitchRule(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 switch member (case or default statement) in a switch rule or
371      * case group is on a single-line.
372      *
373      * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
374      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
375      * @return true if current switch member is single-line statement.
376      */
377     private static boolean isSingleLineSwitchMember(DetailAST statement) {
378         final boolean result;
379         if (isInSwitchRule(statement)) {
380             result = isSingleLineSwitchRule(statement);
381         }
382         else {
383             result = isSingleLineCaseGroup(statement);
384         }
385         return result;
386     }
387 
388     /**
389      * Checks if switch member in case group (case or default statement)
390      * is single-line statement, e.g.:
391      *
392      * <p>
393      * {@code
394      * case 1: System.out.println("case one"); break;
395      * case 2: System.out.println("case two"); break;
396      * case 3: ;
397      * default: System.out.println("default"); break;
398      * }
399      * </p>
400      *
401      *
402      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
403      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
404      * @return true if current switch member is single-line statement.
405      */
406     private static boolean isSingleLineCaseGroup(DetailAST ast) {
407         return Optional.of(ast)
408             .map(DetailAST::getNextSibling)
409             .map(DetailAST::getLastChild)
410             .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
411             .orElse(Boolean.TRUE);
412     }
413 
414     /**
415      * Checks if switch member in switch rule (case or default statement) is
416      * single-line statement, e.g.:
417      *
418      * <p>
419      * {@code
420      * case 1 -> System.out.println("case one");
421      * case 2 -> System.out.println("case two");
422      * default -> System.out.println("default");
423      * }
424      * </p>
425      *
426      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
427      *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
428      * @return true if current switch label is single-line statement.
429      */
430     private static boolean isSingleLineSwitchRule(DetailAST ast) {
431         final DetailAST lastSibling = ast.getParent().getLastChild();
432         return TokenUtil.areOnSameLine(ast, lastSibling);
433     }
434 
435     /**
436      * Checks if current else statement is single-line statement, e.g.:
437      *
438      * <p>
439      * {@code
440      * else doSomeStuff();
441      * }
442      * </p>
443      *
444      * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
445      * @return true if current else statement is single-line statement.
446      */
447     private static boolean isSingleLineElse(DetailAST literalElse) {
448         final DetailAST block = literalElse.getFirstChild();
449         return TokenUtil.areOnSameLine(literalElse, block);
450     }
451 
452 }