blob: e3c7588ebb4e9b8f16a37c2d4e904d3fa262d4e7 [file]
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
use clippy_utils::source::snippet_with_context;
use clippy_utils::sugg::Sugg;
use clippy_utils::{
SpanlessEq, get_parent_expr, higher, is_block_like, is_else_clause, is_parent_stmt, is_receiver_of_method_call,
peel_blocks, peel_blocks_with_stmt, span_contains_comment,
};
use rustc_ast::ast::LitKind;
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::{Block, Expr, ExprKind, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::SyntaxContext;
declare_clippy_lint! {
/// ### What it does
/// Checks for expressions of the form `if c { true } else {
/// false }` (or vice versa) and suggests using the condition directly.
///
/// This also covers the early-return guard form, where a condition returns a
/// tuple-like based on it.
///
/// ### Why is this bad?
/// Redundant code.
///
/// ### Known problems
/// Maybe false positives: Sometimes, the two branches are
/// painstakingly documented (which we, of course, do not detect), so they *may*
/// have some value. Even then, the documentation can be rewritten to match the
/// shorter code.
///
/// ### Example
/// ```no_run
/// # let x = true;
/// if x {
/// false
/// } else {
/// true
/// }
/// # ;
/// ```
///
/// Use instead:
/// ```no_run
/// # let x = true;
/// !x
/// # ;
/// ```
///
/// Or, for the early-return guard form:
/// ```
/// # fn f(c: bool) -> Result<bool, ()> {
/// if c {
/// return Ok(true);
/// }
/// Ok(false)
/// # }
/// ```
///
/// Use instead:
/// ```
/// # fn f(c: bool) -> Result<bool, ()> {
/// Ok(c)
/// # }
/// ```
#[clippy::version = "pre 1.29.0"]
pub NEEDLESS_BOOL,
complexity,
"if-statements with plain booleans in the then- and else-clause, e.g., `if p { true } else { false }`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for expressions of the form `if c { x = true } else { x = false }`
/// (or vice versa) and suggest assigning the variable directly from the
/// condition.
///
/// ### Why is this bad?
/// Redundant code.
///
/// ### Example
/// ```rust,ignore
/// # fn must_keep(x: i32, y: i32) -> bool { x == y }
/// # let x = 32; let y = 10;
/// # let mut skip: bool;
/// if must_keep(x, y) {
/// skip = false;
/// } else {
/// skip = true;
/// }
/// ```
/// Use instead:
/// ```rust,ignore
/// # fn must_keep(x: i32, y: i32) -> bool { x == y }
/// # let x = 32; let y = 10;
/// # let mut skip: bool;
/// skip = !must_keep(x, y);
/// ```
#[clippy::version = "1.71.0"]
pub NEEDLESS_BOOL_ASSIGN,
complexity,
"setting the same boolean variable in both branches of an if-statement"
}
declare_lint_pass!(NeedlessBool => [NEEDLESS_BOOL, NEEDLESS_BOOL_ASSIGN]);
fn condition_needs_parentheses(e: &Expr<'_>) -> bool {
let mut inner = e;
while let ExprKind::Binary(_, i, _)
| ExprKind::Call(i, _)
| ExprKind::Cast(i, _)
| ExprKind::Type(i, _)
| ExprKind::Index(i, _, _) = inner.kind
{
if is_block_like(i) {
return true;
}
inner = i;
}
false
}
impl<'tcx> LateLintPass<'tcx> for NeedlessBool {
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
use self::Expression::{Bool, RetBool};
if !e.span.from_expansion()
&& let Some(higher::If {
cond,
then,
r#else: Some(else_expr),
}) = higher::If::hir(e)
&& !span_contains_comment(cx, e.span)
{
let reduce = |ret, not| {
span_lint_and_then(
cx,
NEEDLESS_BOOL,
e.span,
"this if-then-else expression returns a bool literal",
|diag| {
let mut applicability = Applicability::MachineApplicable;
let snip = Sugg::hir_with_context(cx, cond, e.span.ctxt(), "<predicate>", &mut applicability);
let mut snip = if not { !snip } else { snip };
if ret {
snip = snip.make_return();
}
if is_else_clause(cx.tcx, e) {
snip = snip.blockify();
}
if (condition_needs_parentheses(cond) && is_parent_stmt(cx, e.hir_id))
|| is_receiver_of_method_call(cx, e)
|| is_as_argument(cx, e)
|| is_operand_of_binary_or_unary(cx, e)
{
snip = snip.maybe_paren();
}
diag.span_suggestion(e.span, "you can reduce it to", snip.to_string(), applicability);
},
);
};
if let Some(a) = fetch_bool_block(then)
&& let Some(b) = fetch_bool_block(else_expr)
{
match (a, b) {
(RetBool(true), RetBool(true)) | (Bool(true), Bool(true)) => {
span_lint(
cx,
NEEDLESS_BOOL,
e.span,
"this if-then-else expression will always return true",
);
},
(RetBool(false), RetBool(false)) | (Bool(false), Bool(false)) => {
span_lint(
cx,
NEEDLESS_BOOL,
e.span,
"this if-then-else expression will always return false",
);
},
(RetBool(true), RetBool(false)) => reduce(true, false),
(Bool(true), Bool(false)) => reduce(false, false),
(RetBool(false), RetBool(true)) => reduce(true, true),
(Bool(false), Bool(true)) => reduce(false, true),
_ => (),
}
}
if let Some((lhs_a, a)) = fetch_assign(then)
&& let Some((lhs_b, b)) = fetch_assign(else_expr)
&& SpanlessEq::new(cx).eq_expr(SyntaxContext::root(), lhs_a, lhs_b)
{
let mut applicability = Applicability::MachineApplicable;
let cond = Sugg::hir_with_context(cx, cond, e.span.ctxt(), "..", &mut applicability);
let (lhs, _) = snippet_with_context(cx, lhs_a.span, e.span.ctxt(), "..", &mut applicability);
let mut sugg = if a == b {
format!("{cond}; {lhs} = {a:?};")
} else {
format!("{lhs} = {};", if a { cond } else { !cond })
};
if is_else_clause(cx.tcx, e) {
sugg = format!("{{ {sugg} }}");
}
span_lint_and_sugg(
cx,
NEEDLESS_BOOL_ASSIGN,
e.span,
"this if-then-else expression assigns a bool literal",
"you can reduce it to",
sugg,
applicability,
);
}
}
}
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
// Detect the early-return form:
// if c {
// return Ok(true);
// }
// Ok(false)
// and reduce it to `Ok(c)`. The optional `Ok(..)` wrapper can be any tuple-like
// constructor (or absent), as long as the guard and the trailing expression use the
// same one. Constructors are pure, so folding the condition in keeps the behavior.
if let Some(tail) = block.expr
&& !tail.span.from_expansion()
&& let [.., last_stmt] = block.stmts
&& let StmtKind::Semi(if_expr) | StmtKind::Expr(if_expr) = last_stmt.kind
&& !if_expr.span.from_expansion()
&& let Some(higher::If {
cond,
then,
r#else: None,
}) = higher::If::hir(if_expr)
&& let ExprKind::Ret(Some(ret)) = peel_blocks_with_stmt(then).kind
&& let Some((then_func, then_val)) = fetch_bool_and_wrapper(ret)
&& let Some((tail_func, tail_val)) = fetch_bool_and_wrapper(tail)
// `if c { return Ok(true) } Ok(true)` is always the same value; the condition might
// have side effects, so don't touch it. Comparing the values here is cheap, so do it
// before the constructor resolution in `wrapper_ctors_match`, which needs `qpath_res`.
&& then_val != tail_val
&& wrapper_ctors_match(cx, then_func, tail_func)
{
// If there is another `if _ { return ..; }` before the `if_expr`, don't lint. Replacing the last
// `if` won't decrease complexity, without refactoring the entire block.
if block.stmts.len() >= 2
&& let [.., prev_stmt, _] = block.stmts
&& let StmtKind::Semi(prev_if_expr) | StmtKind::Expr(prev_if_expr) = prev_stmt.kind
&& let Some(higher::If {
cond: _,
then: prev_then,
r#else: None,
}) = higher::If::hir(prev_if_expr)
&& let ExprKind::Ret(Some(_)) = peel_blocks_with_stmt(prev_then).kind
{
return;
}
let span = if_expr.span.to(tail.span);
if span_contains_comment(cx, span) {
return;
}
let mut applicability = Applicability::MachineApplicable;
let mut snip = Sugg::hir_with_context(cx, cond, span.ctxt(), "..", &mut applicability);
// `then_val` is the value returned when the condition holds, so a `false` there means
// the result is the negation of the condition.
if !then_val {
snip = !snip;
}
let sugg = match tail_func {
Some(func) => {
let func_snip = snippet_with_context(cx, func.span, span.ctxt(), "..", &mut applicability).0;
format!("{func_snip}({snip})")
},
None => snip.to_string(),
};
span_lint_and_sugg(
cx,
NEEDLESS_BOOL,
span,
"this `if` guard returns a bool literal and is followed by another",
"you can reduce it to",
sugg,
applicability,
);
}
}
}
/// Returns the optional constructor call wrapping a bool literal (e.g. the `Ok` in `Ok(true)`)
/// along with the bool value. The constructor is returned as the unresolved callee expression;
/// resolving it to a `DefId` (via [`wrapper_ctors_match`]) needs `qpath_res`, which is
/// comparatively expensive, so callers should only do that once the cheap bool-value comparison
/// has passed. A bare bool literal yields `None`.
fn fetch_bool_and_wrapper<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<(Option<&'tcx Expr<'tcx>>, bool)> {
let expr = peel_blocks(expr);
if let Some(value) = fetch_bool_expr(expr) {
return Some((None, value));
}
if let ExprKind::Call(func, [arg]) = expr.kind
&& let Some(value) = fetch_bool_expr(arg)
{
return Some((Some(func), value));
}
None
}
/// Checks whether two optional wrapper-constructor call expressions (as returned by
/// [`fetch_bool_and_wrapper`]) are absent on both sides, or call the same tuple-like constructor.
fn wrapper_ctors_match(cx: &LateContext<'_>, a: Option<&Expr<'_>>, b: Option<&Expr<'_>>) -> bool {
fn resolve_ctor(cx: &LateContext<'_>, func: &Expr<'_>) -> Option<DefId> {
if let ExprKind::Path(qpath) = &func.kind
&& let Res::Def(DefKind::Ctor(..), did) = cx.qpath_res(qpath, func.hir_id)
{
return Some(did);
}
None
}
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => resolve_ctor(cx, a).is_some_and(|a| Some(a) == resolve_ctor(cx, b)),
_ => false,
}
}
enum Expression {
Bool(bool),
RetBool(bool),
}
fn fetch_bool_block(expr: &Expr<'_>) -> Option<Expression> {
match peel_blocks_with_stmt(expr).kind {
ExprKind::Ret(Some(ret)) => Some(Expression::RetBool(fetch_bool_expr(ret)?)),
_ => Some(Expression::Bool(fetch_bool_expr(expr)?)),
}
}
fn fetch_bool_expr(expr: &Expr<'_>) -> Option<bool> {
if let ExprKind::Lit(lit_ptr) = peel_blocks(expr).kind
&& let LitKind::Bool(value) = lit_ptr.node
{
return Some(value);
}
None
}
fn fetch_assign<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, bool)> {
if let ExprKind::Assign(lhs, rhs, _) = peel_blocks_with_stmt(expr).kind {
fetch_bool_expr(rhs).map(|b| (lhs, b))
} else {
None
}
}
fn is_as_argument(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
matches!(get_parent_expr(cx, e).map(|e| e.kind), Some(ExprKind::Cast(_, _)))
}
fn is_operand_of_binary_or_unary(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
matches!(
get_parent_expr(cx, e).map(|e| e.kind),
Some(ExprKind::Binary(..) | ExprKind::Unary(..))
)
}