View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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         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 }