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                 break;
182             case TokenTypes.LITERAL_ELSE:
183                 result = ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
184                 break;
185             case TokenTypes.LAMBDA:
186                 result = !isInSwitchRule(ast);
187                 break;
188             default:
189                 result = true;
190                 break;
191         }
192         return result;
193     }
194 
195     /**
196      * Checks if current loop has empty body and can be skipped by this check.
197      *
198      * @param ast for, while statements.
199      * @return true if current loop can be skipped by check.
200      */
201     private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
202         return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
203     }
204 
205     /**
206      * Checks if switch member (case, default statements) has statements without curly braces.
207      *
208      * @param ast case, default statements.
209      * @return true if switch member has unbraced statements, false otherwise.
210      */
211     private static boolean hasUnbracedStatements(DetailAST ast) {
212         final DetailAST nextSibling = ast.getNextSibling();
213         boolean result = false;
214 
215         if (isInSwitchRule(ast)) {
216             final DetailAST parent = ast.getParent();
217             result = parent.getLastChild().getType() != TokenTypes.SLIST;
218         }
219         else if (nextSibling != null
220             && nextSibling.getType() == TokenTypes.SLIST
221             && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
222             result = true;
223         }
224         return result;
225     }
226 
227     /**
228      * Checks if current statement can be skipped by "need braces" warning.
229      *
230      * @param statement if, for, while, do-while, lambda, else, case, default statements.
231      * @return true if current statement can be skipped by Check.
232      */
233     private boolean isSkipStatement(DetailAST statement) {
234         return allowSingleLineStatement && isSingleLineStatement(statement);
235     }
236 
237     /**
238      * Checks if current statement is single-line statement, e.g.:
239      * <p>
240      * {@code
241      * if (obj.isValid()) return true;
242      * }
243      * </p>
244      * <p>
245      * {@code
246      * while (obj.isValid()) return true;
247      * }
248      * </p>
249      *
250      * @param statement if, for, while, do-while, lambda, else, case, default statements.
251      * @return true if current statement is single-line statement.
252      */
253     private static boolean isSingleLineStatement(DetailAST statement) {
254         final boolean result;
255 
256         switch (statement.getType()) {
257             case TokenTypes.LITERAL_IF:
258                 result = isSingleLineIf(statement);
259                 break;
260             case TokenTypes.LITERAL_FOR:
261                 result = isSingleLineFor(statement);
262                 break;
263             case TokenTypes.LITERAL_DO:
264                 result = isSingleLineDoWhile(statement);
265                 break;
266             case TokenTypes.LITERAL_WHILE:
267                 result = isSingleLineWhile(statement);
268                 break;
269             case TokenTypes.LAMBDA:
270                 result = !isInSwitchRule(statement)
271                     && isSingleLineLambda(statement);
272                 break;
273             case TokenTypes.LITERAL_CASE:
274             case TokenTypes.LITERAL_DEFAULT:
275                 result = isSingleLineSwitchMember(statement);
276                 break;
277             default:
278                 result = isSingleLineElse(statement);
279                 break;
280         }
281 
282         return result;
283     }
284 
285     /**
286      * Checks if current while statement is single-line statement, e.g.:
287      * <p>
288      * {@code
289      * while (obj.isValid()) return true;
290      * }
291      * </p>
292      *
293      * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
294      * @return true if current while statement is single-line statement.
295      */
296     private static boolean isSingleLineWhile(DetailAST literalWhile) {
297         boolean result = false;
298         if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
299             final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
300             result = TokenUtil.areOnSameLine(literalWhile, block);
301         }
302         return result;
303     }
304 
305     /**
306      * Checks if current do-while statement is single-line statement, e.g.:
307      * <p>
308      * {@code
309      * do this.notify(); while (o != null);
310      * }
311      * </p>
312      *
313      * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
314      * @return true if current do-while statement is single-line statement.
315      */
316     private static boolean isSingleLineDoWhile(DetailAST literalDo) {
317         boolean result = false;
318         if (literalDo.getParent().getType() == TokenTypes.SLIST) {
319             final DetailAST block = literalDo.getFirstChild();
320             result = TokenUtil.areOnSameLine(block, literalDo);
321         }
322         return result;
323     }
324 
325     /**
326      * Checks if current for statement is single-line statement, e.g.:
327      * <p>
328      * {@code
329      * for (int i = 0; ; ) this.notify();
330      * }
331      * </p>
332      *
333      * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
334      * @return true if current for statement is single-line statement.
335      */
336     private static boolean isSingleLineFor(DetailAST literalFor) {
337         boolean result = false;
338         if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
339             result = true;
340         }
341         else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
342             result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
343         }
344         return result;
345     }
346 
347     /**
348      * Checks if current if statement is single-line statement, e.g.:
349      * <p>
350      * {@code
351      * if (obj.isValid()) return true;
352      * }
353      * </p>
354      *
355      * @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
356      * @return true if current if statement is single-line statement.
357      */
358     private static boolean isSingleLineIf(DetailAST literalIf) {
359         boolean result = false;
360         if (literalIf.getParent().getType() == TokenTypes.SLIST) {
361             final DetailAST literalIfLastChild = literalIf.getLastChild();
362             final DetailAST block;
363             if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
364                 block = literalIfLastChild.getPreviousSibling();
365             }
366             else {
367                 block = literalIfLastChild;
368             }
369             final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
370             result = TokenUtil.areOnSameLine(ifCondition, block);
371         }
372         return result;
373     }
374 
375     /**
376      * Checks if current lambda statement is single-line statement, e.g.:
377      * <p>
378      * {@code
379      * Runnable r = () -> System.out.println("Hello, world!");
380      * }
381      * </p>
382      *
383      * @param lambda {@link TokenTypes#LAMBDA lambda statement}.
384      * @return true if current lambda statement is single-line statement.
385      */
386     private static boolean isSingleLineLambda(DetailAST lambda) {
387         final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
388         return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
389     }
390 
391     /**
392      * Looks for the last token in lambda.
393      *
394      * @param lambda token to check.
395      * @return last token in lambda
396      */
397     private static DetailAST getLastLambdaToken(DetailAST lambda) {
398         DetailAST node = lambda;
399         do {
400             node = node.getLastChild();
401         } while (node.getLastChild() != null);
402         return node;
403     }
404 
405     /**
406      * Checks if current ast's parent is a switch rule, e.g.:
407      * <p>
408      * {@code
409      * case 1 ->  monthString = "January";
410      * }
411      * </p>
412      *
413      * @param ast the ast to check.
414      * @return true if current ast belongs to a switch rule.
415      */
416     private static boolean isInSwitchRule(DetailAST ast) {
417         return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
418     }
419 
420     /**
421      * Checks if switch member (case or default statement) in a switch rule or
422      * case group is on a single-line.
423      *
424      * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
425      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
426      * @return true if current switch member is single-line statement.
427      */
428     private static boolean isSingleLineSwitchMember(DetailAST statement) {
429         final boolean result;
430         if (isInSwitchRule(statement)) {
431             result = isSingleLineSwitchRule(statement);
432         }
433         else {
434             result = isSingleLineCaseGroup(statement);
435         }
436         return result;
437     }
438 
439     /**
440      * Checks if switch member in case group (case or default statement)
441      * is single-line statement, e.g.:
442      * <p>
443      * {@code
444      * case 1: System.out.println("case one"); break;
445      * case 2: System.out.println("case two"); break;
446      * case 3: ;
447      * default: System.out.println("default"); break;
448      * }
449      * </p>
450      *
451      *
452      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
453      *     {@link TokenTypes#LITERAL_DEFAULT default statement}.
454      * @return true if current switch member is single-line statement.
455      */
456     private static boolean isSingleLineCaseGroup(DetailAST ast) {
457         return Optional.of(ast)
458             .map(DetailAST::getNextSibling)
459             .map(DetailAST::getLastChild)
460             .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
461             .orElse(Boolean.TRUE);
462     }
463 
464     /**
465      * Checks if switch member in switch rule (case or default statement) is
466      * single-line statement, e.g.:
467      * <p>
468      * {@code
469      * case 1 -> System.out.println("case one");
470      * case 2 -> System.out.println("case two");
471      * default -> System.out.println("default");
472      * }
473      * </p>
474      *
475      * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
476      *            {@link TokenTypes#LITERAL_DEFAULT default statement}.
477      * @return true if current switch label is single-line statement.
478      */
479     private static boolean isSingleLineSwitchRule(DetailAST ast) {
480         final DetailAST lastSibling = ast.getParent().getLastChild();
481         return TokenUtil.areOnSameLine(ast, lastSibling);
482     }
483 
484     /**
485      * Checks if current else statement is single-line statement, e.g.:
486      * <p>
487      * {@code
488      * else doSomeStuff();
489      * }
490      * </p>
491      *
492      * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
493      * @return true if current else statement is single-line statement.
494      */
495     private static boolean isSingleLineElse(DetailAST literalElse) {
496         final DetailAST block = literalElse.getFirstChild();
497         return TokenUtil.areOnSameLine(literalElse, block);
498     }
499 
500 }