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 }