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 }