blob: 43c62e1e131a3eed4452fcd52994b34a7429739c [file] [log] [blame] [edit]
use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt, integer_const, is_zero_integer_const};
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{ExprUseNode, clip, expr_use_ctxt, peel_hir_expr_refs, unsext};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::{BinOpKind, Expr, ExprKind, Node, Path, QPath};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::{Span, SyntaxContext, kw};
use super::IDENTITY_OP;
pub(crate) fn check<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'_>,
op: BinOpKind,
left: &'tcx Expr<'_>,
right: &'tcx Expr<'_>,
) {
if !is_allowed(cx, expr, op, left, right) {
return;
}
// we need to know whether a ref is coerced to a value
// if a ref is coerced, then the suggested lint must deref it
// e.g. `let _: i32 = x+0` with `x: &i32` should be replaced with `let _: i32 = *x`.
// we do this by checking the _kind_ of the type of the expression
// if it's a ref, we then check whether it is erased, and that's it.
let (peeled_left_span, left_is_coerced_to_value) = {
let expr = peel_hir_expr_refs(left).0;
let span = expr.span;
let is_coerced = expr_is_erased_ref(cx, expr);
(span, is_coerced)
};
let (peeled_right_span, right_is_coerced_to_value) = {
let expr = peel_hir_expr_refs(right).0;
let span = expr.span;
let is_coerced = expr_is_erased_ref(cx, expr);
(span, is_coerced)
};
let ctxt = expr.span.ctxt();
match op {
BinOpKind::Add | BinOpKind::BitOr | BinOpKind::BitXor => {
if is_redundant_op(cx, left, 0, ctxt) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, 0, ctxt) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sub => {
if is_redundant_op(cx, right, 0, ctxt) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Mul => {
if is_redundant_op(cx, left, 1, ctxt) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, 1, ctxt) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Div => {
if is_redundant_op(cx, right, 1, ctxt) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::BitAnd => {
if is_redundant_op(cx, left, -1, ctxt) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, -1, ctxt) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span),
_ => (),
}
}
fn expr_is_erased_ref(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
match cx.typeck_results().expr_ty(expr).kind() {
ty::Ref(r, ..) => r.is_erased(),
_ => false,
}
}
#[derive(Copy, Clone)]
enum Parens {
Needed,
Unneeded,
}
/// Checks if a binary expression needs parenthesis when reduced to just its
/// right or left child.
///
/// e.g. `-(x + y + 0)` cannot be reduced to `-x + y`, as the behavior changes silently.
/// e.g. `1u64 + ((x + y + 0i32) as u64)` cannot be reduced to `1u64 + x + y as u64`, since
/// the cast expression will not apply to the same expression.
/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }` cannot be reduced
/// to `if b { 1 } else { 2 } + if b { 3 } else { 4 }` where the `if` could be
/// interpreted as a statement. The same behavior happens for `match`, `loop`,
/// and blocks.
/// e.g. `2 * (0 + { a })` can be reduced to `2 * { a }` without the need for parenthesis,
/// but `1 * ({ a } + 4)` cannot be reduced to `{ a } + 4`, as a block at the start of a line
/// will be interpreted as a statement instead of an expression.
///
/// See #8724, #13470
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, child: &Expr<'_>) -> Parens {
match child.kind {
ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) => {
// For casts and binary expressions, we want to add parenthesis if
// the parent HIR node is an expression, or if the parent HIR node
// is a Block or Stmt, and the new left hand side would need
// parenthesis be treated as a statement rather than an expression.
if let Some((_, parent)) = cx.tcx.hir_parent_iter(binary.hir_id).next() {
match parent {
Node::Expr(_) => return Parens::Needed,
Node::Block(_) | Node::Stmt(_) => {
// ensure we're checking against the leftmost expression of `child`
//
// ~~~~~~~~~~~ `binary`
// ~~~ `lhs`
// 0 + {4} * 2
// ~~~~~~~ `child`
return needs_parenthesis(cx, binary, lhs);
},
_ => return Parens::Unneeded,
}
}
},
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {
// For if, match, block, and loop expressions, we want to add parenthesis if
// the closest ancestor node that is not an expression is a block or statement.
// This would mean that the rustfix suggestion will appear at the start of a line, which causes
// these expressions to be interpreted as statements if they do not have parenthesis.
let mut prev_id = binary.hir_id;
for (_, parent) in cx.tcx.hir_parent_iter(binary.hir_id) {
if let Node::Expr(expr) = parent
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) | ExprKind::Unary(_, lhs) = expr.kind
&& lhs.hir_id == prev_id
{
// keep going until we find a node that encompasses left of `binary`
prev_id = expr.hir_id;
continue;
}
match parent {
Node::Block(_) | Node::Stmt(_) => return Parens::Needed,
_ => return Parens::Unneeded,
};
}
},
_ => {
return Parens::Unneeded;
},
}
Parens::Needed
}
fn is_allowed<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
cmp: BinOpKind,
left: &Expr<'tcx>,
right: &Expr<'tcx>,
) -> bool {
// Exclude case where the left or right side is associated function call returns a type which is
// `Self` that is not given explicitly, and the expression is not a let binding's init
// expression and the let binding has a type annotation, or a function's return value.
if (is_assoc_fn_without_type_instance(cx, left) || is_assoc_fn_without_type_instance(cx, right))
&& !is_expr_used_with_type_annotation(cx, expr)
{
return false;
}
// This lint applies to integers and their references
cx.typeck_results().expr_ty(left).peel_refs().is_integral()
&& cx.typeck_results().expr_ty(right).peel_refs().is_integral()
// `1 << 0` is a common pattern in bit manipulation code
&& !(cmp == BinOpKind::Shl
&& is_zero_integer_const(cx, right, expr.span.ctxt())
&& integer_const(cx, left, expr.span.ctxt()) == Some(1))
}
fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span: Span, arg: Span) {
let ecx = ConstEvalCtxt::new(cx);
let ctxt = span.ctxt();
if match (ecx.eval_full_int(left, ctxt), ecx.eval_full_int(right, ctxt)) {
(Some(FullInt::S(lv)), Some(FullInt::S(rv))) => lv.abs() < rv.abs(),
(Some(FullInt::U(lv)), Some(FullInt::U(rv))) => lv < rv,
_ => return,
} {
span_ineffective_operation(cx, span, arg, Parens::Unneeded, false);
}
}
fn is_redundant_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, ctxt: SyntaxContext) -> bool {
if let Some(Constant::Int(v)) = ConstEvalCtxt::new(cx).eval_local(e, ctxt).map(Constant::peel_refs) {
let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() {
ty::Int(ity) => unsext(cx.tcx, -1_i128, ity),
ty::Uint(uty) => clip(cx.tcx, !0, uty),
_ => return false,
};
if match m {
0 => v == 0,
-1 => v == check,
1 => v == 1,
_ => unreachable!(),
} {
return true;
}
}
false
}
fn span_ineffective_operation(
cx: &LateContext<'_>,
span: Span,
arg: Span,
parens: Parens,
is_ref_coerced_to_val: bool,
) {
let mut applicability = Applicability::MachineApplicable;
let expr_snippet = snippet_with_applicability(cx, arg, "..", &mut applicability);
let expr_snippet = if is_ref_coerced_to_val {
format!("*{expr_snippet}")
} else {
expr_snippet.into_owned()
};
let suggestion = match parens {
Parens::Needed => format!("({expr_snippet})"),
Parens::Unneeded => expr_snippet,
};
span_lint_and_sugg(
cx,
IDENTITY_OP,
span,
"this operation has no effect",
"consider reducing it to",
suggestion,
applicability,
);
}
fn is_expr_used_with_type_annotation<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
match expr_use_ctxt(cx, expr).use_node(cx) {
ExprUseNode::LetStmt(letstmt) => letstmt.ty.is_some(),
ExprUseNode::Return(_) => true,
_ => false,
}
}
/// Check if the expression is an associated function without a type instance.
/// Example:
/// ```
/// trait Def {
/// fn def() -> Self;
/// }
/// impl Def for usize {
/// fn def() -> Self {
/// 0
/// }
/// }
/// fn test() {
/// let _ = 0usize + &Default::default();
/// let _ = 0usize + &Def::def();
/// }
/// ```
fn is_assoc_fn_without_type_instance<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
if let ExprKind::Call(func, _) = peel_hir_expr_refs(expr).0.kind
&& let ExprKind::Path(QPath::Resolved(
// If it's not None, don't need to go further.
None,
Path {
res: Res::Def(DefKind::AssocFn, def_id),
..
},
)) = func.kind
&& let output_ty = cx.tcx.fn_sig(def_id).instantiate_identity().skip_binder().output()
&& let ty::Param(ty::ParamTy {
name: kw::SelfUpper, ..
}) = output_ty.kind()
{
return true;
}
false
}