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 }