use crate::rule_prelude::*;
use ast::*;
use SyntaxKind::*;
declare_lint! {
#[derive(Default)]
#[serde(default)]
NoExtraBooleanCast,
errors,
"no-extra-boolean-cast",
pub enforce_for_logical_operands: bool,
}
const BOOL_NODE_KINDS: [SyntaxKind; 5] = [IF_STMT, DO_WHILE_STMT, WHILE_STMT, COND_EXPR, FOR_STMT];
#[derive(Debug)]
enum Reason {
ExplicitBoolean(SyntaxNode),
ImplicitCast(SyntaxNode),
LogicalNotCast(SyntaxToken),
}
#[typetag::serde]
impl CstRule for NoExtraBooleanCast {
fn check_node(&self, node: &SyntaxNode, ctx: &mut RuleCtx) -> Option<()> {
match node.kind() {
UNARY_EXPR => {
let expr = node.to::<UnaryExpr>();
let child = skip_grouping(expr.syntax().first_child(), SyntaxNode::first_child)
.next()?
.try_to::<Expr>()?;
if expr.op()? != op![!]
|| !matches!(child, Expr::UnaryExpr(expr) if expr.op()? == op![!])
{
return None;
}
if let Some(reason) = in_bool_ctx(node, self.enforce_for_logical_operands) {
let err = ctx.err(self.name(), "redundant double negation").primary(
expr.op_token().unwrap().text_range(),
"this operator is redundant...",
);
ctx.add_err(reason_labels(err, reason));
}
}
CALL_EXPR => {
if !util::constructor_or_call_with_callee(node, "Boolean") {
return None;
}
if let Some(reason) = in_bool_ctx(node, self.enforce_for_logical_operands) {
let err = ctx.err(self.name(), "redundant `Boolean` call").primary(
node.trimmed_range(),
"this call to `Boolean` is redundant...",
);
ctx.add_err(reason_labels(err, reason));
}
}
_ => {}
}
None
}
}
fn reason_labels(builder: DiagnosticBuilder, reason: Reason) -> DiagnosticBuilder {
match reason {
Reason::ExplicitBoolean(node) => builder.secondary(
node.trimmed_range(),
"...because `Boolean` already creates a boolean value",
),
Reason::ImplicitCast(node) => builder.secondary(
node.trimmed_range(),
"...because this condition already implicitly coerces to a boolean",
),
Reason::LogicalNotCast(token) => builder.secondary(
token.text_range(),
"...because this operator already coerces to a boolean",
),
}
}
fn in_bool_ctx(node: &SyntaxNode, enforce_logical: bool) -> Option<Reason> {
let parent = skip_grouping(node.parent(), SyntaxNode::parent).nth(1);
if let Some(parent) = parent {
if util::constructor_or_call_with_callee(parent.clone(), "Boolean") {
return parent
.child_with_kind(ARG_LIST)
.filter(|cond| {
skip_grouping(cond.first_child(), SyntaxNode::first_child)
.next()
.as_ref()
== Some(node)
})
.map(|_| Reason::ExplicitBoolean(parent));
}
}
if let Some(casted_node) = implicitly_casted_node(node) {
let cond_node = match casted_node.kind() {
IF_STMT | DO_WHILE_STMT | WHILE_STMT => casted_node
.child_with_kind(CONDITION)
.and_then(|n| n.first_child()),
FOR_STMT => casted_node.child_with_kind(FOR_STMT_TEST)?.first_child(),
COND_EXPR => casted_node
.to::<CondExpr>()
.test()
.map(|x| x.syntax().clone()),
_ => None,
};
return cond_node
.filter(|inner| {
skip_grouping(inner.clone(), SyntaxNode::first_child)
.next()
.as_ref()
== Some(node)
})
.map(Reason::ImplicitCast);
}
let parent = skip_grouping(node.parent(), SyntaxNode::parent).next()?;
if let Some(unexpr) = parent.try_to::<UnaryExpr>() {
let (tok, op) = unexpr.op_details()?;
if op == op![!] {
return Some(Reason::LogicalNotCast(tok));
}
}
if enforce_logical {
let expr = parent.try_to::<BinExpr>()?;
expr.op()
.and_then(|op| match op {
op if op == op![||] || op == op![&&] => Some(()),
_ => None,
})
.and_then(|_| in_bool_ctx(expr.syntax(), true))
} else {
None
}
}
fn skip_grouping<F>(
child: impl Into<Option<SyntaxNode>>,
successor: F,
) -> impl Iterator<Item = SyntaxNode>
where
F: FnMut(&SyntaxNode) -> Option<SyntaxNode>,
{
std::iter::successors(child.into(), successor).filter(|node| node.kind() != GROUPING_EXPR)
}
fn implicitly_casted_node(node: &SyntaxNode) -> Option<SyntaxNode> {
let parent = skip_grouping(node.parent(), SyntaxNode::parent).next();
if matches!(
parent.map(|parent| parent.kind()),
Some(CONDITION) | Some(FOR_STMT_TEST)
) {
skip_grouping(node.parent(), SyntaxNode::parent)
.nth(1)
.filter(|node| BOOL_NODE_KINDS.contains(&node.kind()))
} else {
skip_grouping(node.parent(), SyntaxNode::parent)
.next()
.filter(|node| BOOL_NODE_KINDS.contains(&node.kind()))
}
}
rule_tests! {
NoExtraBooleanCast::default(),
err: {
"if (!!foo) {}",
"do {} while (!!foo)",
"while (!!foo) {}",
"!!foo ? bar : baz",
"for (; !!foo;) {}",
"!!!foo",
"Boolean(!!foo)",
"new Boolean(!!foo)",
"if (Boolean(foo)) {}",
"do {} while (Boolean(foo))",
"while (Boolean(foo)) {}",
"Boolean(foo) ? bar : baz",
"for (; Boolean(foo);) {}",
"!Boolean(foo)",
"!Boolean(foo && bar)",
"!Boolean(foo + bar)",
"!Boolean(+foo)",
"!Boolean(foo())",
"!Boolean(foo = bar)",
"!Boolean(...foo);",
"!Boolean(foo, bar());",
"!Boolean((foo, bar()));",
"!Boolean();",
"!(Boolean());",
"if (!Boolean()) { foo() }",
"while (!Boolean()) { foo() }",
"if (Boolean()) { foo() }",
"while (Boolean()) { foo() }",
"Boolean(Boolean(foo))",
"Boolean(!!foo, bar)",
"x=!!a ? b : c ",
"void!Boolean()",
"void! Boolean()",
"typeof!Boolean()",
"(!Boolean())",
"+!Boolean()",
"void !Boolean()",
"void(!Boolean())",
"void/**/!Boolean()",
"!/**/!!foo",
"!!/**/!foo",
"!!!/**/foo",
"!!!foo/**/",
"if(!/**/!foo);",
"(!!/**/foo ? 1 : 2)",
"!/**/Boolean(foo)",
"!Boolean/**/(foo)",
"!Boolean(/**/foo)",
"!Boolean(foo/**/)",
"!Boolean(foo)/**/",
"if(Boolean/**/(foo));",
"(Boolean(foo/**/) ? 1 : 2)",
"/**/!Boolean()",
"!/**/Boolean()",
"!Boolean/**/()",
"!Boolean(/**/)",
"!Boolean()/**/",
"if(!/**/Boolean());",
"(!Boolean(/**/) ? 1 : 2)",
"if(/**/Boolean());",
"if(Boolean/**/());",
"if(Boolean(/**/));",
"if(Boolean()/**/);",
"(Boolean/**/() ? 1 : 2)",
"Boolean(!!(a, b))",
"Boolean(Boolean((a, b)))",
"Boolean((!!(a, b)))",
"Boolean((Boolean((a, b))))",
"Boolean(!(!(a, b)))",
"Boolean((!(!(a, b))))",
"Boolean(!!(a = b))",
"Boolean((!!(a = b)))",
"Boolean(Boolean(a = b))",
"Boolean(Boolean((a += b)))",
"Boolean(!!(a === b))",
"Boolean(!!((a !== b)))",
"Boolean(!!a.b)",
"Boolean(Boolean((a)))",
"Boolean((!!(a)))",
"new Boolean(!!(a, b))",
"new Boolean(Boolean((a, b)))",
"new Boolean((!!(a, b)))",
"new Boolean((Boolean((a, b))))",
"new Boolean(!(!(a, b)))",
"new Boolean((!(!(a, b))))",
"new Boolean(!!(a = b))",
"new Boolean((!!(a = b)))",
"new Boolean(Boolean(a = b))",
"new Boolean(Boolean((a += b)))",
"new Boolean(!!(a === b))",
"new Boolean(!!((a !== b)))",
"new Boolean(!!a.b)",
"new Boolean(Boolean((a)))",
"new Boolean((!!(a)))",
"if (!!(a, b));",
"if (Boolean((a, b)));",
"if (!(!(a, b)));",
"if (!!(a = b));",
"if (Boolean(a = b));",
"if (!!(a > b));",
"if (Boolean(a === b));",
"if (!!f(a));",
"if (Boolean(f(a)));",
"if (!!(f(a)));",
"if ((!!f(a)));",
"if ((Boolean(f(a))));",
"if (!!a);",
"if (Boolean(a));",
"while (!!(a, b));",
"while (Boolean((a, b)));",
"while (!(!(a, b)));",
"while (!!(a = b));",
"while (Boolean(a = b));",
"while (!!(a > b));",
"while (Boolean(a === b));",
"while (!!f(a));",
"while (Boolean(f(a)));",
"while (!!(f(a)));",
"while ((!!f(a)));",
"while ((Boolean(f(a))));",
"while (!!a);",
"while (Boolean(a));",
"do {} while (!!(a, b));",
"do {} while (Boolean((a, b)));",
"do {} while (!(!(a, b)));",
"do {} while (!!(a = b));",
"do {} while (Boolean(a = b));",
"do {} while (!!(a > b));",
"do {} while (!!f(a));",
"do {} while (Boolean(f(a)));",
"do {} while (!!(f(a)));",
"do {} while ((!!f(a)));",
"do {} while ((Boolean(f(a))));",
"do {} while (!!a);",
"do {} while (Boolean(a));",
"for (; !!(a, b););",
"for (; Boolean((a, b)););",
"for (; !(!(a, b)););",
"for (; !!(a = b););",
"for (; Boolean(a = b););",
"for (; !!(a > b););",
"for (; Boolean(a === b););",
"for (; !!f(a););",
"for (; Boolean(f(a)););",
"for (; !!(f(a)););",
"for (; (!!f(a)););",
"for (; (Boolean(f(a))););",
"for (; !!a;);",
"for (; Boolean(a););",
"!!(a, b) ? c : d",
"(!!(a, b)) ? c : d",
"Boolean((a, b)) ? c : d",
"!!(a = b) ? c : d",
"Boolean(a -= b) ? c : d",
"(Boolean((a *= b))) ? c : d",
"!!(a ? b : c) ? d : e",
"Boolean(a ? b : c) ? d : e",
"!!(a || b) ? c : d",
"Boolean(a && b) ? c : d",
"!!(a === b) ? c : d",
"Boolean(a < b) ? c : d",
"!!((a !== b)) ? c : d",
"Boolean((a >= b)) ? c : d",
"!!+a ? b : c",
"!!+(a) ? b : c",
"Boolean(!a) ? b : c",
"!!f(a) ? b : c",
"(!!f(a)) ? b : c",
"Boolean(a.b) ? c : d",
"!!a ? b : c",
"Boolean(a) ? b : c",
"!!!(a, b)",
"!Boolean((a, b))",
"!!!(a = b)",
"!!(!(a += b))",
"!(!!(a += b))",
"!Boolean(a -= b)",
"!Boolean((a -= b))",
"!(Boolean(a -= b))",
"!!!(a || b)",
"!Boolean(a || b)",
"!!!(a && b)",
"!Boolean(a && b)",
"!!!(a != b)",
"!!!(a === b)",
"var x = !Boolean(a > b)",
"!!!(a - b)",
"!!!(a ** b)",
"!Boolean(a ** b)",
"!Boolean(!a)",
"!Boolean((!a))",
"!Boolean(!(a))",
"!(Boolean(!a))",
"!!!+a",
"!!!(+a)",
"!!(!+a)",
"!(!!+a)",
"!Boolean((-a))",
"!Boolean(-(a))",
"!!!(--a)",
"!Boolean(a++)",
"!!!f(a)",
"!!!(f(a))",
"!!!a",
"!Boolean(a)",
"!Boolean(!!a)",
"!Boolean(Boolean(a))",
"!Boolean(Boolean(!!a))",
"while (a) { if (!!b) {} }",
"while (a) { if (Boolean(b)) {} }",
"if (a) { const b = !!!c; }",
"if (a) { const b = !Boolean(c); }",
"for (let a = 0; a < n; a++) { if (!!b) {} }",
"for (let a = 0; a < n; a++) { if (Boolean(b)) {} }",
"do { const b = !!!c; } while(a)",
"do { const b = !Boolean(c); } while(a)",
},
ok: {
"Boolean(bar, !!baz);",
"var foo = !!bar;",
"function foo() { return !!bar; }",
"var foo = bar() ? !!baz : !!bat",
"for(!!foo;;) {}",
"for(;; !!foo) {}",
"var foo = Boolean(bar);",
"function foo() { return Boolean(bar); }",
"var foo = bar() ? Boolean(baz) : Boolean(bat)",
"for(Boolean(foo);;) {}",
"for(;; Boolean(foo)) {}",
"if (new Boolean(foo)) {}"
}
}
rule_tests! {
logical_operands_valid,
logical_operands_invalid,
NoExtraBooleanCast { enforce_for_logical_operands: true },
err: {
"if (!!foo || bar) {}",
"while (!!foo && bar) {}",
"if ((!!foo || bar) && baz) {}",
"foo && Boolean(bar) ? baz : bat",
"var foo = new Boolean(!!bar || baz)"
},
ok: {
"if (foo || bar) {}",
"while (foo && bar) {}",
"if ((foo || bar) && baz) {}",
"foo && bar ? baz : bat",
"var foo = new Boolean(bar || baz)",
"var foo = !!bar || baz;"
}
}