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