NeedBracesCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2021 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.blocks;
import java.util.Optional;
import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* <p>
* Checks for braces around code blocks.
* </p>
* <ul>
* <li>
* Property {@code allowSingleLineStatement} - allow single-line statements without braces.
* Type is {@code boolean}.
* Default value is {@code false}.
* </li>
* <li>
* Property {@code allowEmptyLoopBody} - allow loops with empty bodies.
* Type is {@code boolean}.
* Default value is {@code false}.
* </li>
* <li>
* Property {@code tokens} - tokens to check
* Type is {@code java.lang.String[]}.
* Validation type is {@code tokenSet}.
* Default value is:
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
* LITERAL_DO</a>,
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE">
* LITERAL_ELSE</a>,
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
* LITERAL_FOR</a>,
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
* LITERAL_IF</a>,
* <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
* LITERAL_WHILE</a>.
* </li>
* </ul>
* <p>
* To configure the check:
* </p>
* <pre>
* <module name="NeedBraces"/>
* </pre>
* <p>Example:</p>
* <pre>
* if (obj.isValid()) return true; // violation, single-line statements not allowed without braces
* if (true) { // OK
* return true;
* } else // violation, single-line statements not allowed without braces
* return false;
* for (int i = 0; i < 5; i++) { // OK
* ++count;
* }
* do // violation, single-line statements not allowed without braces
* ++count;
* while (false);
* for (int j = 0; j < 10; j++); // violation, empty loop body not allowed
* for(int i = 0; i < 10; value.incrementValue()); // violation, empty loop body not allowed
* while (counter < 10) // violation, single-line statements not allowed without braces
* ++count;
* while (value.incrementValue() < 5); // violation, empty loop body not allowed
* switch (num) {
* case 1: counter++; break; // OK
* }
* </pre>
* <p>
* To configure the check for {@code if} and {@code else} blocks:
* </p>
* <pre>
* <module name="NeedBraces">
* <property name="tokens" value="LITERAL_IF, LITERAL_ELSE"/>
* </module>
* </pre>
* <p>Example:</p>
* <pre>
* if (obj.isValid()) return true; // violation, single-line statements not allowed without braces
* if (true) { // OK
* return true;
* } else // violation, single-line statements not allowed without braces
* return false;
* for (int i = 0; i < 5; i++) { // OK
* ++count;
* }
* do // OK
* ++count;
* while (false);
* for (int j = 0; j < 10; j++); // OK
* for(int i = 0; i < 10; value.incrementValue()); // OK
* while (counter < 10) // OK
* ++count;
* while (value.incrementValue() < 5); // OK
* switch (num) {
* case 1: counter++; break; // OK
* }
* </pre>
* <p>
* To configure the check to allow single-line statements
* ({@code if, while, do-while, for}) without braces:
* </p>
* <pre>
* <module name="NeedBraces">
* <property name="allowSingleLineStatement" value="true"/>
* <property name="tokens"
* value="LITERAL_IF, LITERAL_WHILE, LITERAL_DO, LITERAL_FOR"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* if (obj.isValid()) return true; // OK
* if (true) { // OK
* return true;
* } else // OK
* return false;
* for (int i = 0; i < 5; i++) { // OK
* ++count;
* }
* do // OK
* ++count;
* while (false);
* for (int j = 0; j < 10; j++); // violation, empty loop body not allowed
* for(int i = 0; i < 10; value.incrementValue()); // violation, empty loop body not allowed
* while (counter < 10) // OK
* ++count;
* while (value.incrementValue() < 5); // violation, empty loop body not allowed
* switch (num) {
* case 1: counter++; break; // OK
* }
* while (obj.isValid()) return true; // OK
* do this.notify(); while (o != null); // OK
* for (int i = 0; ; ) this.notify(); // OK
* </pre>
* <p>
* To configure the check to allow {@code case, default} single-line statements without braces:
* </p>
* <pre>
* <module name="NeedBraces">
* <property name="tokens" value="LITERAL_CASE, LITERAL_DEFAULT"/>
* <property name="allowSingleLineStatement" value="true"/>
* </module>
* </pre>
* <p>
* Next statements won't be violated by check:
* </p>
* <pre>
* if (obj.isValid()) return true; // OK
* if (true) { // OK
* return true;
* } else // OK
* return false;
* for (int i = 0; i < 5; i++) { // OK
* ++count;
* }
* do // OK
* ++count;
* while (false);
* for (int j = 0; j < 10; j++); // OK
* for(int i = 0; i < 10; value.incrementValue()); // OK
* while (counter < 10) // OK
* ++count;
* while (value.incrementValue() < 5); // OK
* switch (num) {
* case 1: counter++; break; // OK
* case 6: counter += 10; break; // OK
* default: counter = 100; break; // OK
* }
* </pre>
* <p>
* To configure the check to allow loops ({@code while, for}) with empty bodies:
* </p>
* <pre>
* <module name="NeedBraces">
* <property name="allowEmptyLoopBody" value="true"/>
* <property name="tokens" value="LITERAL_WHILE, LITERAL_FOR"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* if (obj.isValid()) return true; // OK
* if (true) { // OK
* return true;
* } else // OK
* return false;
* for (int i = 0; i < 5; i++) { // OK
* ++count;
* }
* do // OK
* ++count;
* while (false);
* for (int j = 0; j < 10; j++); // OK
* for(int i = 0; i < 10; value.incrementValue()); // OK
* while (counter < 10) // violation, single-line statements not allowed without braces
* ++count;
* while (value.incrementValue() < 5); // OK
* switch (num) {
* case 1: counter++; break; // OK
* }
* </pre>
* <p>
* To configure the check to lambdas:
* </p>
* <pre>
* <module name="NeedBraces">
* <property name="tokens" value="LAMBDA"/>
* <property name="allowSingleLineStatement" value="true"/>
* </module>
* </pre>
* <p>
* Results in following:
* </p>
* <pre>
* allowedFuture.addCallback(result -> assertEquals("Invalid response",
* EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS), result), // violation, lambda spans 2 lines
* ex -> fail(ex.getMessage())); // OK
*
* allowedFuture.addCallback(result -> {
* return assertEquals("Invalid response",
* EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS), result);
* }, // OK
* ex -> fail(ex.getMessage()));
* </pre>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code needBraces}
* </li>
* </ul>
*
* @since 3.0
*/
@StatelessCheck
public class NeedBracesCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY_NEED_BRACES = "needBraces";
/**
* Allow single-line statements without braces.
*/
private boolean allowSingleLineStatement;
/**
* Allow loops with empty bodies.
*/
private boolean allowEmptyLoopBody;
/**
* Setter to allow single-line statements without braces.
*
* @param allowSingleLineStatement Check's option for skipping single-line statements
*/
public void setAllowSingleLineStatement(boolean allowSingleLineStatement) {
this.allowSingleLineStatement = allowSingleLineStatement;
}
/**
* Setter to allow loops with empty bodies.
*
* @param allowEmptyLoopBody Check's option for allowing loops with empty body.
*/
public void setAllowEmptyLoopBody(boolean allowEmptyLoopBody) {
this.allowEmptyLoopBody = allowEmptyLoopBody;
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.LITERAL_DO,
TokenTypes.LITERAL_ELSE,
TokenTypes.LITERAL_FOR,
TokenTypes.LITERAL_IF,
TokenTypes.LITERAL_WHILE,
};
}
@Override
public int[] getAcceptableTokens() {
return new int[] {
TokenTypes.LITERAL_DO,
TokenTypes.LITERAL_ELSE,
TokenTypes.LITERAL_FOR,
TokenTypes.LITERAL_IF,
TokenTypes.LITERAL_WHILE,
TokenTypes.LITERAL_CASE,
TokenTypes.LITERAL_DEFAULT,
TokenTypes.LAMBDA,
};
}
@Override
public int[] getRequiredTokens() {
return CommonUtil.EMPTY_INT_ARRAY;
}
@Override
public void visitToken(DetailAST ast) {
final boolean hasNoSlist = ast.findFirstToken(TokenTypes.SLIST) == null;
if (hasNoSlist && !isSkipStatement(ast) && isBracesNeeded(ast)) {
log(ast, MSG_KEY_NEED_BRACES, ast.getText());
}
}
/**
* Checks if token needs braces.
* Some tokens have additional conditions:
* <ul>
* <li>{@link TokenTypes#LITERAL_FOR}</li>
* <li>{@link TokenTypes#LITERAL_WHILE}</li>
* <li>{@link TokenTypes#LITERAL_CASE}</li>
* <li>{@link TokenTypes#LITERAL_DEFAULT}</li>
* <li>{@link TokenTypes#LITERAL_ELSE}</li>
* <li>{@link TokenTypes#LAMBDA}</li>
* </ul>
* For all others default value {@code true} is returned.
*
* @param ast token to check
* @return result of additional checks for specific token types,
* {@code true} if there is no additional checks for token
*/
private boolean isBracesNeeded(DetailAST ast) {
final boolean result;
switch (ast.getType()) {
case TokenTypes.LITERAL_FOR:
case TokenTypes.LITERAL_WHILE:
result = !isEmptyLoopBodyAllowed(ast);
break;
case TokenTypes.LITERAL_CASE:
case TokenTypes.LITERAL_DEFAULT:
result = hasUnbracedStatements(ast)
&& !isSwitchLabeledExpression(ast);
break;
case TokenTypes.LITERAL_ELSE:
result = ast.findFirstToken(TokenTypes.LITERAL_IF) == null;
break;
case TokenTypes.LAMBDA:
result = !isInSwitchRule(ast);
break;
default:
result = true;
break;
}
return result;
}
/**
* Checks if current loop has empty body and can be skipped by this check.
*
* @param ast for, while statements.
* @return true if current loop can be skipped by check.
*/
private boolean isEmptyLoopBodyAllowed(DetailAST ast) {
return allowEmptyLoopBody && ast.findFirstToken(TokenTypes.EMPTY_STAT) != null;
}
/**
* Checks if switch member (case, default statements) has statements without curly braces.
*
* @param ast case, default statements.
* @return true if switch member has unbraced statements, false otherwise.
*/
private static boolean hasUnbracedStatements(DetailAST ast) {
final DetailAST nextSibling = ast.getNextSibling();
boolean result = false;
if (isInSwitchRule(ast)) {
final DetailAST parent = ast.getParent();
result = parent.getLastChild().getType() != TokenTypes.SLIST;
}
else if (nextSibling != null
&& nextSibling.getType() == TokenTypes.SLIST
&& nextSibling.getFirstChild().getType() != TokenTypes.SLIST) {
result = true;
}
return result;
}
/**
* Checks if current statement can be skipped by "need braces" warning.
*
* @param statement if, for, while, do-while, lambda, else, case, default statements.
* @return true if current statement can be skipped by Check.
*/
private boolean isSkipStatement(DetailAST statement) {
return allowSingleLineStatement && isSingleLineStatement(statement);
}
/**
* Checks if current statement is single-line statement, e.g.:
* <p>
* {@code
* if (obj.isValid()) return true;
* }
* </p>
* <p>
* {@code
* while (obj.isValid()) return true;
* }
* </p>
*
* @param statement if, for, while, do-while, lambda, else, case, default statements.
* @return true if current statement is single-line statement.
*/
private static boolean isSingleLineStatement(DetailAST statement) {
final boolean result;
switch (statement.getType()) {
case TokenTypes.LITERAL_IF:
result = isSingleLineIf(statement);
break;
case TokenTypes.LITERAL_FOR:
result = isSingleLineFor(statement);
break;
case TokenTypes.LITERAL_DO:
result = isSingleLineDoWhile(statement);
break;
case TokenTypes.LITERAL_WHILE:
result = isSingleLineWhile(statement);
break;
case TokenTypes.LAMBDA:
result = !isInSwitchRule(statement)
&& isSingleLineLambda(statement);
break;
case TokenTypes.LITERAL_CASE:
case TokenTypes.LITERAL_DEFAULT:
result = isSingleLineSwitchMember(statement);
break;
default:
result = isSingleLineElse(statement);
break;
}
return result;
}
/**
* Checks if current while statement is single-line statement, e.g.:
* <p>
* {@code
* while (obj.isValid()) return true;
* }
* </p>
*
* @param literalWhile {@link TokenTypes#LITERAL_WHILE while statement}.
* @return true if current while statement is single-line statement.
*/
private static boolean isSingleLineWhile(DetailAST literalWhile) {
boolean result = false;
if (literalWhile.getParent().getType() == TokenTypes.SLIST) {
final DetailAST block = literalWhile.getLastChild().getPreviousSibling();
result = TokenUtil.areOnSameLine(literalWhile, block);
}
return result;
}
/**
* Checks if current do-while statement is single-line statement, e.g.:
* <p>
* {@code
* do this.notify(); while (o != null);
* }
* </p>
*
* @param literalDo {@link TokenTypes#LITERAL_DO do-while statement}.
* @return true if current do-while statement is single-line statement.
*/
private static boolean isSingleLineDoWhile(DetailAST literalDo) {
boolean result = false;
if (literalDo.getParent().getType() == TokenTypes.SLIST) {
final DetailAST block = literalDo.getFirstChild();
result = TokenUtil.areOnSameLine(block, literalDo);
}
return result;
}
/**
* Checks if current for statement is single-line statement, e.g.:
* <p>
* {@code
* for (int i = 0; ; ) this.notify();
* }
* </p>
*
* @param literalFor {@link TokenTypes#LITERAL_FOR for statement}.
* @return true if current for statement is single-line statement.
*/
private static boolean isSingleLineFor(DetailAST literalFor) {
boolean result = false;
if (literalFor.getLastChild().getType() == TokenTypes.EMPTY_STAT) {
result = true;
}
else if (literalFor.getParent().getType() == TokenTypes.SLIST) {
result = TokenUtil.areOnSameLine(literalFor, literalFor.getLastChild());
}
return result;
}
/**
* Checks if current if statement is single-line statement, e.g.:
* <p>
* {@code
* if (obj.isValid()) return true;
* }
* </p>
*
* @param literalIf {@link TokenTypes#LITERAL_IF if statement}.
* @return true if current if statement is single-line statement.
*/
private static boolean isSingleLineIf(DetailAST literalIf) {
boolean result = false;
if (literalIf.getParent().getType() == TokenTypes.SLIST) {
final DetailAST literalIfLastChild = literalIf.getLastChild();
final DetailAST block;
if (literalIfLastChild.getType() == TokenTypes.LITERAL_ELSE) {
block = literalIfLastChild.getPreviousSibling();
}
else {
block = literalIfLastChild;
}
final DetailAST ifCondition = literalIf.findFirstToken(TokenTypes.EXPR);
result = TokenUtil.areOnSameLine(ifCondition, block);
}
return result;
}
/**
* Checks if current lambda statement is single-line statement, e.g.:
* <p>
* {@code
* Runnable r = () -> System.out.println("Hello, world!");
* }
* </p>
*
* @param lambda {@link TokenTypes#LAMBDA lambda statement}.
* @return true if current lambda statement is single-line statement.
*/
private static boolean isSingleLineLambda(DetailAST lambda) {
final DetailAST lastLambdaToken = getLastLambdaToken(lambda);
return TokenUtil.areOnSameLine(lambda, lastLambdaToken);
}
/**
* Looks for the last token in lambda.
*
* @param lambda token to check.
* @return last token in lambda
*/
private static DetailAST getLastLambdaToken(DetailAST lambda) {
DetailAST node = lambda;
do {
node = node.getLastChild();
} while (node.getLastChild() != null);
return node;
}
/**
* Checks if current ast's parent is a switch rule, e.g.:
* <p>
* {@code
* case 1 -> monthString = "January";
* }
* </p>
*
* @param ast the ast to check.
* @return true if current ast belongs to a switch rule.
*/
private static boolean isInSwitchRule(DetailAST ast) {
return ast.getParent().getType() == TokenTypes.SWITCH_RULE;
}
/**
* Checks if current expression is a switch labeled expression. If so,
* braces are not allowed e.g.:
* <p>
* {@code
* case 1 -> 4;
* }
* </p>
*
* @param ast the ast to check
* @return true if current expression is a switch labeled expression.
*/
private static boolean isSwitchLabeledExpression(DetailAST ast) {
final DetailAST parent = ast.getParent();
return switchRuleHasSingleExpression(parent);
}
/**
* Checks if current switch labeled expression contains only a single expression.
*
* @param switchRule {@link TokenTypes#SWITCH_RULE}.
* @return true if current switch rule has a single expression.
*/
private static boolean switchRuleHasSingleExpression(DetailAST switchRule) {
final DetailAST possibleExpression = switchRule.findFirstToken(TokenTypes.EXPR);
return possibleExpression != null
&& possibleExpression.getFirstChild().getFirstChild() == null;
}
/**
* Checks if switch member (case or default statement) in a switch rule or
* case group is on a single line.
*
* @param statement {@link TokenTypes#LITERAL_CASE case statement} or
* {@link TokenTypes#LITERAL_DEFAULT default statement}.
* @return true if current switch member is single-line statement.
*/
private static boolean isSingleLineSwitchMember(DetailAST statement) {
final boolean result;
if (isInSwitchRule(statement)) {
result = isSingleLineSwitchRule(statement);
}
else {
result = isSingleLineCaseGroup(statement);
}
return result;
}
/**
* Checks if switch member in case group (case or default statement)
* is single-line statement, e.g.:
* <p>
* {@code
* case 1: System.out.println("case one"); break;
* case 2: System.out.println("case two"); break;
* case 3: ;
* default: System.out.println("default"); break;
* }
* </p>
*
*
* @param ast {@link TokenTypes#LITERAL_CASE case statement} or
* {@link TokenTypes#LITERAL_DEFAULT default statement}.
* @return true if current switch member is single-line statement.
*/
private static boolean isSingleLineCaseGroup(DetailAST ast) {
return Optional.of(ast)
.map(DetailAST::getNextSibling)
.map(DetailAST::getLastChild)
.map(lastToken -> TokenUtil.areOnSameLine(ast, lastToken))
.orElse(true);
}
/**
* Checks if switch member in switch rule (case or default statement) is
* single-line statement, e.g.:
* <p>
* {@code
* case 1 -> System.out.println("case one");
* case 2 -> System.out.println("case two");
* default -> System.out.println("default");
* }
* </p>
*
* @param ast {@link TokenTypes#LITERAL_CASE case statement} or
* {@link TokenTypes#LITERAL_DEFAULT default statement}.
* @return true if current switch label is single-line statement.
*/
private static boolean isSingleLineSwitchRule(DetailAST ast) {
final DetailAST lastSibling = ast.getParent().getLastChild();
return TokenUtil.areOnSameLine(ast, lastSibling);
}
/**
* Checks if current else statement is single-line statement, e.g.:
* <p>
* {@code
* else doSomeStuff();
* }
* </p>
*
* @param literalElse {@link TokenTypes#LITERAL_ELSE else statement}.
* @return true if current else statement is single-line statement.
*/
private static boolean isSingleLineElse(DetailAST literalElse) {
final DetailAST block = literalElse.getFirstChild();
return TokenUtil.areOnSameLine(literalElse, block);
}
}