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