1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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 *
40 * @since 3.0
41 */
42 @StatelessCheck
43 public class NeedBracesCheck extends AbstractCheck {
44
45 /**
46 * A key is pointing to the warning message text in "messages.properties"
47 * file.
48 */
49 public static final String MSG_KEY_NEED_BRACES = "needBraces";
50
51 /**
52 * Allow single-line statements without braces.
53 */
54 private boolean allowSingleLineStatement;
55
56 /**
57 * Allow loops with empty bodies.
58 */
59 private boolean allowEmptyLoopBody;
60
61 /**
62 * Creates a new {@code NeedBracesCheck} instance.
63 */
64 public NeedBracesCheck() {
65 // no code by default
66 }
67
68 /**
69 * Setter to allow single-line statements without braces.
70 *
71 * @param allowSingleLineStatement Check's option for skipping single-line statements
72 * @since 6.5
73 */
74 public void setAllowSingleLineStatement(boolean allowSingleLineStatement) {
75 this.allowSingleLineStatement = allowSingleLineStatement;
76 }
77
78 /**
79 * Setter to allow loops with empty bodies.
80 *
81 * @param allowEmptyLoopBody Check's option for allowing loops with empty body.
82 * @since 6.12.1
83 */
84 public void setAllowEmptyLoopBody(boolean allowEmptyLoopBody) {
85 this.allowEmptyLoopBody = allowEmptyLoopBody;
86 }
87
88 @Override
89 public int[] getDefaultTokens() {
90 return new int[] {
91 TokenTypes.LITERAL_DO,
92 TokenTypes.LITERAL_ELSE,
93 TokenTypes.LITERAL_FOR,
94 TokenTypes.LITERAL_IF,
95 TokenTypes.LITERAL_WHILE,
96 };
97 }
98
99 @Override
100 public int[] getAcceptableTokens() {
101 return new int[] {
102 TokenTypes.LITERAL_DO,
103 TokenTypes.LITERAL_ELSE,
104 TokenTypes.LITERAL_FOR,
105 TokenTypes.LITERAL_IF,
106 TokenTypes.LITERAL_WHILE,
107 TokenTypes.LITERAL_CASE,
108 TokenTypes.LITERAL_DEFAULT,
109 TokenTypes.LAMBDA,
110 };
111 }
112
113 @Override
114 public int[] getRequiredTokens() {
115 return CommonUtil.EMPTY_INT_ARRAY;
116 }
117
118 @Override
119 public void visitToken(DetailAST ast) {
120 final boolean hasNoSlist = ast.findFirstToken(TokenTypes.SLIST) == null;
121 if (hasNoSlist && !isSkipStatement(ast) && isBracesNeeded(ast)) {
122 log(ast, MSG_KEY_NEED_BRACES, ast.getText());
123 }
124 }
125
126 /**
127 * Checks if token needs braces.
128 * Some tokens have additional conditions:
129 * <ul>
130 * <li>{@link TokenTypes#LITERAL_FOR}</li>
131 * <li>{@link TokenTypes#LITERAL_WHILE}</li>
132 * <li>{@link TokenTypes#LITERAL_CASE}</li>
133 * <li>{@link TokenTypes#LITERAL_DEFAULT}</li>
134 * <li>{@link TokenTypes#LITERAL_ELSE}</li>
135 * <li>{@link TokenTypes#LAMBDA}</li>
136 * </ul>
137 * For all others default value {@code true} is returned.
138 *
139 * @param ast token to check
140 * @return result of additional checks for specific token types,
141 * {@code true} if there is no additional checks for token
142 */
143 private boolean isBracesNeeded(DetailAST ast) {
144 return switch (ast.getType()) {
145 case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE -> !isEmptyLoopBodyAllowed(ast);
146 case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> hasUnbracedStatements(ast);
147 case TokenTypes.LITERAL_ELSE -> ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
148 case TokenTypes.LAMBDA -> !isInSwitchRule(ast);
149 default -> true;
150 };
151 }
152
153 /**
154 * Checks if current loop has empty body and can be skipped by this check.
155 *
156 * @param ast for, while statements.
157 * @return true if current loop can be skipped by check.
158 */
159 private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
160 return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
161 }
162
163 /**
164 * Checks if switch member (case, default statements) has statements without curly braces.
165 *
166 * @param ast case, default statements.
167 * @return true if switch member has unbraced statements, false otherwise.
168 */
169 private static boolean hasUnbracedStatements(DetailAST ast) {
170 final DetailAST nextSibling = ast.getNextSibling();
171 boolean result = false;
172
173 if (isInSwitchRule(ast)) {
174 final DetailAST parent = ast.getParent();
175 result = parent.getLastChild().getType() != TokenTypes.SLIST;
176 }
177 else if (nextSibling != null
178 && nextSibling.getType() == TokenTypes.SLIST
179 && nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
180 result = true;
181 }
182 return result;
183 }
184
185 /**
186 * Checks if current statement can be skipped by "need braces" warning.
187 *
188 * @param statement if, for, while, do-while, lambda, else, case, default statements.
189 * @return true if current statement can be skipped by Check.
190 */
191 private boolean isSkipStatement(DetailAST statement) {
192 return allowSingleLineStatement && isSingleLineStatement(statement);
193 }
194
195 /**
196 * Checks if current statement is single-line statement, e.g.:
197 *
198 * <p>
199 * {@code
200 * if (obj.isValid()) return true;
201 * }
202 * </p>
203 *
204 * <p>
205 * {@code
206 * while (obj.isValid()) return true;
207 * }
208 * </p>
209 *
210 * @param statement if, for, while, do-while, lambda, else, case, default statements.
211 * @return true if current statement is single-line statement.
212 */
213 private static boolean isSingleLineStatement(DetailAST statement) {
214
215 return switch (statement.getType()) {
216 case TokenTypes.LITERAL_IF -> isSingleLineIf(statement);
217 case TokenTypes.LITERAL_FOR -> isSingleLineFor(statement);
218 case TokenTypes.LITERAL_DO -> isSingleLineDoWhile(statement);
219 case TokenTypes.LITERAL_WHILE -> isSingleLineWhile(statement);
220 case TokenTypes.LAMBDA -> !isInSwitchRule(statement)
221 && isSingleLineLambda(statement);
222 case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT ->
223 isSingleLineSwitchMember(statement);
224 default -> isSingleLineElse(statement);
225 };
226 }
227
228 /**
229 * Checks if current while statement is single-line statement, e.g.:
230 *
231 * <p>
232 * {@code
233 * while (obj.isValid()) return true;
234 * }
235 * </p>
236 *
237 * @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
238 * @return true if current while statement is single-line statement.
239 */
240 private static boolean isSingleLineWhile(DetailAST literalWhile) {
241 boolean result = false;
242 if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
243 final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
244 result = TokenUtil.areOnSameLine(literalWhile, block);
245 }
246 return result;
247 }
248
249 /**
250 * Checks if current do-while statement is single-line statement, e.g.:
251 *
252 * <p>
253 * {@code
254 * do this.notify(); while (o != null);
255 * }
256 * </p>
257 *
258 * @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
259 * @return true if current do-while statement is single-line statement.
260 */
261 private static boolean isSingleLineDoWhile(DetailAST literalDo) {
262 boolean result = false;
263 if (literalDo.getParent().getType() == TokenTypes.SLIST) {
264 final DetailAST block = literalDo.getFirstChild();
265 result = TokenUtil.areOnSameLine(block, literalDo);
266 }
267 return result;
268 }
269
270 /**
271 * Checks if current for statement is single-line statement, e.g.:
272 *
273 * <p>
274 * {@code
275 * for (int i = 0; ; ) this.notify();
276 * }
277 * </p>
278 *
279 * @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
280 * @return true if current for statement is single-line statement.
281 */
282 private static boolean isSingleLineFor(DetailAST literalFor) {
283 boolean result = false;
284 if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
285 result = true;
286 }
287 else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
288 result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
289 }
290 return result;
291 }
292
293 /**
294 * Checks if current if statement is single-line statement, e.g.:
295 *
296 * <p>
297 * {@code
298 * if (obj.isValid()) return true;
299 * }
300 * </p>
301 *
302 * @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
303 * @return true if current if statement is single-line statement.
304 */
305 private static boolean isSingleLineIf(DetailAST literalIf) {
306 boolean result = false;
307 if (literalIf.getParent().getType() == TokenTypes.SLIST) {
308 final DetailAST literalIfLastChild = literalIf.getLastChild();
309 final DetailAST block;
310 if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
311 block = literalIfLastChild.getPreviousSibling();
312 }
313 else {
314 block = literalIfLastChild;
315 }
316 final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
317 result = TokenUtil.areOnSameLine(ifCondition, block);
318 }
319 return result;
320 }
321
322 /**
323 * Checks if current lambda statement is single-line statement, e.g.:
324 *
325 * <p>
326 * {@code
327 * Runnable r = () -> System.out.println("Hello, world!");
328 * }
329 * </p>
330 *
331 * @param lambda {@link TokenTypes#LAMBDA lambda statement}.
332 * @return true if current lambda statement is single-line statement.
333 */
334 private static boolean isSingleLineLambda(DetailAST lambda) {
335 final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
336 return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
337 }
338
339 /**
340 * Looks for the last token in lambda.
341 *
342 * @param lambda token to check.
343 * @return last token in lambda
344 */
345 private static DetailAST getLastLambdaToken(DetailAST lambda) {
346 DetailAST node = lambda;
347 do {
348 node = node.getLastChild();
349 } while (node.getLastChild() != null);
350 return node;
351 }
352
353 /**
354 * Checks if current ast's parent is a switch rule, e.g.:
355 *
356 * <p>
357 * {@code
358 * case 1 -> monthString = "January";
359 * }
360 * </p>
361 *
362 * @param ast the ast to check.
363 * @return true if current ast belongs to a switch rule.
364 */
365 private static boolean isInSwitchRule(DetailAST ast) {
366 return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
367 }
368
369 /**
370 * Checks if switch member (case or default statement) in a switch rule or
371 * case group is on a single-line.
372 *
373 * @param statement {@link TokenTypes#LITERAL_CASE case statement} or
374 * {@link TokenTypes#LITERAL_DEFAULT default statement}.
375 * @return true if current switch member is single-line statement.
376 */
377 private static boolean isSingleLineSwitchMember(DetailAST statement) {
378 final boolean result;
379 if (isInSwitchRule(statement)) {
380 result = isSingleLineSwitchRule(statement);
381 }
382 else {
383 result = isSingleLineCaseGroup(statement);
384 }
385 return result;
386 }
387
388 /**
389 * Checks if switch member in case group (case or default statement)
390 * is single-line statement, e.g.:
391 *
392 * <p>
393 * {@code
394 * case 1: System.out.println("case one"); break;
395 * case 2: System.out.println("case two"); break;
396 * case 3: ;
397 * default: System.out.println("default"); break;
398 * }
399 * </p>
400 *
401 *
402 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
403 * {@link TokenTypes#LITERAL_DEFAULT default statement}.
404 * @return true if current switch member is single-line statement.
405 */
406 private static boolean isSingleLineCaseGroup(DetailAST ast) {
407 return Optional.of(ast)
408 .map(DetailAST::getNextSibling)
409 .map(DetailAST::getLastChild)
410 .map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
411 .orElse(Boolean.TRUE);
412 }
413
414 /**
415 * Checks if switch member in switch rule (case or default statement) is
416 * single-line statement, e.g.:
417 *
418 * <p>
419 * {@code
420 * case 1 -> System.out.println("case one");
421 * case 2 -> System.out.println("case two");
422 * default -> System.out.println("default");
423 * }
424 * </p>
425 *
426 * @param ast {@link TokenTypes#LITERAL_CASE case statement} or
427 * {@link TokenTypes#LITERAL_DEFAULT default statement}.
428 * @return true if current switch label is single-line statement.
429 */
430 private static boolean isSingleLineSwitchRule(DetailAST ast) {
431 final DetailAST lastSibling = ast.getParent().getLastChild();
432 return TokenUtil.areOnSameLine(ast, lastSibling);
433 }
434
435 /**
436 * Checks if current else statement is single-line statement, e.g.:
437 *
438 * <p>
439 * {@code
440 * else doSomeStuff();
441 * }
442 * </p>
443 *
444 * @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
445 * @return true if current else statement is single-line statement.
446 */
447 private static boolean isSingleLineElse(DetailAST literalElse) {
448 final DetailAST block = literalElse.getFirstChild();
449 return TokenUtil.areOnSameLine(literalElse, block);
450 }
451
452 }