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