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