extern crate rustc_ast;
extern crate rustc_hir;
extern crate rustc_middle;
extern crate rustc_span;
use std::collections::HashMap;
use std::collections::HashSet;
use rustc_ast::ByRef;
use rustc_ast::LitKind;
use rustc_hir::BinOpKind;
use rustc_hir::Expr;
use rustc_hir::ExprKind;
use rustc_hir::HirId;
use rustc_hir::MatchSource;
use rustc_hir::Mutability;
use rustc_hir::Node;
use rustc_hir::Pat;
use rustc_hir::UnOp;
use rustc_hir::attrs::lang_items::LangItem;
use rustc_hir::intravisit::FnKind;
use rustc_hir::intravisit::Visitor;
use rustc_lint::LateContext;
use rustc_lint::LateLintPass;
use rustc_middle::ty::BorrowKind;
use rustc_middle::ty::UpvarCapture;
use rustc_span::Span;
use rustc_span::Symbol;
use crate::diagnostics;
use crate::shared;
crate::declare_late_lint! {
pub REQUIRE_ZEROED_BEFORE_CLOSE,
Deny,
"account close should be preceded by zeroing the same account's data"
}
const TARGET_METHODS: &[&str] = &["close_with_recipient", "close"];
const TARGET_NEEDLES: &[&str] = &["process", "process_instruction", "instruction"];
impl<'tcx> LateLintPass<'tcx> for RequireZeroedBeforeClose {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
_: FnKind<'tcx>,
_: &'tcx rustc_hir::FnDecl<'tcx>,
body: &'tcx rustc_hir::Body<'tcx>,
_: rustc_span::Span,
def_id: rustc_hir::def_id::LocalDefId,
) {
let def_path = cx.tcx.def_path_str(def_id.to_def_id());
if shared::should_skip_def_path(&def_path)
|| !shared::def_path_matches(&def_path, TARGET_NEEDLES)
{
return;
}
let mut collector = Collector::new(cx);
collector.visit_body(body);
let analysis = collector.finish();
for close in &analysis.closes {
if analysis.is_zeroed_before(close) {
continue;
}
diagnostics::emit(cx, REQUIRE_ZEROED_BEFORE_CLOSE, |diag| {
diag.span(close.span);
diag.primary_message(
"account close should be preceded by zeroing the same account's data",
);
diag.help(
"close with `account.close_account_zeroed(&ID, recipient)?` (or the \
`CloseAccountZeroed` builder), which zeroes the data and closes in one step",
);
diag.help(
"to keep a separate close, first clear the whole data buffer of the same \
account with `account.try_borrow_mut()?.fill(0);`",
);
diag.help(
"known limits: after the fill, writes through `pina`, `pinocchio`, or \
`solana_account_view` methods, CPIs, and separately obtained handles to the \
account are not tracked, nor are lends after the close inside a loop",
);
});
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct AccountPlace {
root: HirId,
fields: Vec<Symbol>,
}
struct ZeroFill<'tcx> {
order: usize,
account: &'tcx Expr<'tcx>,
buffer: Option<HirId>,
branches: Vec<HirId>,
}
struct Close<'tcx> {
order: usize,
call: HirId,
span: Span,
receiver: Option<&'tcx Expr<'tcx>>,
branches: Vec<HirId>,
}
struct Lend<'tcx> {
order: usize,
lent: Lent<'tcx>,
own_close: Option<HirId>,
}
enum Lent<'tcx> {
Place(&'tcx Expr<'tcx>),
Binding(HirId),
}
enum Reach {
Place(AccountPlace),
Anywhere,
}
impl Reach {
fn covers(&self, closed: &AccountPlace) -> bool {
match self {
Self::Place(place) => {
place.root == closed.root && closed.fields.starts_with(&place.fields)
}
Self::Anywhere => true,
}
}
}
struct Borrow<'tcx> {
order: usize,
account: &'tcx Expr<'tcx>,
}
struct BufferUse {
order: usize,
buffer: HirId,
path: HirId,
is_drop: bool,
}
struct Analysis<'tcx> {
aliases: HashMap<HirId, &'tcx Expr<'tcx>>,
unstable: HashSet<HirId>,
zero_fills: Vec<ZeroFill<'tcx>>,
closes: Vec<Close<'tcx>>,
borrows: Vec<Borrow<'tcx>>,
buffer_uses: Vec<BufferUse>,
lends: Vec<Lend<'tcx>>,
}
impl<'tcx> Analysis<'tcx> {
fn is_zeroed_before(&self, close: &Close<'tcx>) -> bool {
let Some(closed) = close.receiver.and_then(|receiver| self.place_of(receiver)) else {
return false;
};
let is_lent = self.lends.iter().any(|lend| {
let reach = match lend.lent {
Lent::Place(place) => self.reach_of(place, &mut HashSet::new()),
Lent::Binding(binding) => self.reach_of_binding(binding, &mut HashSet::new()),
};
lend.order < close.order && lend.own_close != Some(close.call) && reach.covers(&closed)
});
if is_lent {
return false;
}
let is_between =
|order: usize, fill: &ZeroFill<'tcx>| fill.order < order && order < close.order;
self.zero_fills.iter().any(|fill| {
fill.order < close.order
&& close.branches.starts_with(&fill.branches)
&& fill
.buffer
.is_none_or(|buffer| !self.unstable.contains(&buffer))
&& self.place_of(fill.account).as_ref() == Some(&closed)
&& !self.borrows.iter().any(|borrow| {
is_between(borrow.order, fill)
&& self
.reach_of(borrow.account, &mut HashSet::new())
.covers(&closed)
}) && !self.buffer_uses.iter().any(|buffer_use| {
is_between(buffer_use.order, fill)
&& Some(buffer_use.buffer) == fill.buffer
&& !buffer_use.is_drop
})
})
}
fn reach_of(&self, expr: &Expr<'_>, visited: &mut HashSet<HirId>) -> Reach {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner) | ExprKind::AddrOf(_, _, inner) => {
self.reach_of(inner, visited)
}
ExprKind::Field(base, field) => {
match self.reach_of(base, visited) {
Reach::Place(mut place) => {
place.fields.push(field.name);
Reach::Place(place)
}
Reach::Anywhere => Reach::Anywhere,
}
}
_ => {
match local_path_binding(expr) {
Some(binding) => self.reach_of_binding(binding, visited),
None => Reach::Anywhere,
}
}
}
}
fn reach_of_binding(&self, binding: HirId, visited: &mut HashSet<HirId>) -> Reach {
match self.aliases.get(&binding) {
Some(_) if self.unstable.contains(&binding) || !visited.insert(binding) => {
Reach::Anywhere
}
Some(initializer) => self.reach_of(initializer, visited),
None => {
Reach::Place(AccountPlace {
root: binding,
fields: Vec::new(),
})
}
}
}
fn place_of(&self, expr: &Expr<'_>) -> Option<AccountPlace> {
self.place_of_inner(expr, &mut HashSet::new())
}
fn place_of_inner(
&self,
expr: &Expr<'_>,
visited: &mut HashSet<HirId>,
) -> Option<AccountPlace> {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner) | ExprKind::AddrOf(_, _, inner) => {
self.place_of_inner(inner, visited)
}
ExprKind::Field(base, field) => {
let mut place = self.place_of_inner(base, visited)?;
place.fields.push(field.name);
Some(place)
}
_ => {
let binding = local_path_binding(expr)?;
if self.unstable.contains(&binding) || !visited.insert(binding) {
return None;
}
match self.aliases.get(&binding) {
Some(initializer) => self.place_of_inner(initializer, visited),
None => {
Some(AccountPlace {
root: binding,
fields: Vec::new(),
})
}
}
}
}
}
}
struct Collector<'cx, 'tcx> {
cx: &'cx LateContext<'tcx>,
order: usize,
branches: Vec<HirId>,
buffers: HashMap<HirId, &'tcx Expr<'tcx>>,
alias_borrows: HashSet<HirId>,
exposures: Vec<(HirId, HirId)>,
projection_uses: HashSet<HirId>,
local_uses: Vec<(HirId, HirId)>,
assigned: Vec<HirId>,
analysis: Analysis<'tcx>,
}
impl<'cx, 'tcx> Collector<'cx, 'tcx> {
fn new(cx: &'cx LateContext<'tcx>) -> Self {
Self {
cx,
order: 0,
branches: Vec::new(),
buffers: HashMap::new(),
alias_borrows: HashSet::new(),
exposures: Vec::new(),
projection_uses: HashSet::new(),
local_uses: Vec::new(),
assigned: Vec::new(),
analysis: Analysis {
aliases: HashMap::new(),
unstable: HashSet::new(),
zero_fills: Vec::new(),
closes: Vec::new(),
borrows: Vec::new(),
buffer_uses: Vec::new(),
lends: Vec::new(),
},
}
}
fn finish(mut self) -> Analysis<'tcx> {
for (alias, exposed) in &self.exposures {
let escapes = self
.local_uses
.iter()
.any(|(binding, path)| binding == alias && !self.projection_uses.contains(path));
if escapes {
self.analysis.unstable.insert(*exposed);
}
}
for binding in std::mem::take(&mut self.assigned) {
let mut current = Some(binding);
let mut visited = HashSet::new();
while let Some(binding) = current
&& visited.insert(binding)
{
self.analysis.unstable.insert(binding);
current = self
.analysis
.aliases
.get(&binding)
.and_then(|initializer| place_root_binding(initializer));
}
}
self.analysis
}
fn next_order(&mut self) -> usize {
self.order += 1;
self.order
}
fn visit_branch(&mut self, expr: &'tcx Expr<'tcx>) {
self.branches.push(expr.hir_id);
self.visit_expr(expr);
self.branches.pop();
}
fn record(&mut self, expr: &'tcx Expr<'tcx>) {
match expr.kind {
ExprKind::Assign(target, ..) | ExprKind::AssignOp(_, target, _) => {
if let Some(binding) = assignment_base_binding(target) {
self.assigned.push(binding);
}
}
ExprKind::AddrOf(_, Mutability::Mut, inner)
if !self.alias_borrows.contains(&expr.hir_id) =>
{
if let Some(binding) = slot_binding(inner) {
self.analysis.unstable.insert(binding);
}
}
ExprKind::Closure(closure) => {
if let Some(upvars) = self.cx.tcx.upvars_mentioned(closure.def_id) {
self.analysis.unstable.extend(upvars.keys().copied());
}
self.record_closure_lends(closure.def_id);
}
ExprKind::Field(base, _) => self.note_projection(base),
ExprKind::Call(callee, [argument]) if is_drop(self.cx, callee) => {
for buffer_use in &mut self.analysis.buffer_uses {
if buffer_use.path == argument.hir_id {
buffer_use.is_drop = true;
}
}
}
ExprKind::Call(callee, arguments) => self.record_call(expr, callee, arguments),
ExprKind::MethodCall(segment, receiver, arguments, _) => {
self.note_projection(receiver);
self.record_method_call(expr, segment.ident.name, receiver, arguments);
}
_ => {
if let Some(binding) = local_path_binding(expr) {
self.record_lend(expr);
self.local_uses.push((binding, expr.hir_id));
if self.buffers.contains_key(&binding) {
let order = self.next_order();
self.analysis.buffer_uses.push(BufferUse {
order,
buffer: binding,
path: expr.hir_id,
is_drop: false,
});
}
}
}
}
}
fn record_call(
&mut self,
expr: &'tcx Expr<'tcx>,
callee: &'tcx Expr<'tcx>,
arguments: &'tcx [Expr<'tcx>],
) {
if let Some(account) = account_borrow(self.cx, expr) {
let order = self.next_order();
self.analysis.borrows.push(Borrow { order, account });
} else if callee_name(callee).is_some_and(|name| TARGET_METHODS.contains(&name.as_str())) {
let order = self.next_order();
self.analysis.closes.push(Close {
order,
call: expr.hir_id,
span: expr.span,
receiver: arguments.first(),
branches: self.branches.clone(),
});
}
}
fn record_closure_lends(&mut self, closure: rustc_hir::def_id::LocalDefId) {
let lent = self
.cx
.typeck_results()
.closure_min_captures_flattened(closure)
.filter(|capture| {
match capture.info.capture_kind {
UpvarCapture::ByRef(kind) => kind != BorrowKind::Immutable,
UpvarCapture::ByValue | UpvarCapture::ByUse => {
capture.place.ty().ref_mutability() == Some(Mutability::Mut)
}
}
})
.map(|capture| capture.get_root_variable())
.collect::<Vec<_>>();
for binding in lent {
let order = self.next_order();
self.analysis.lends.push(Lend {
order,
lent: Lent::Binding(binding),
own_close: None,
});
}
}
fn record_method_call(
&mut self,
expr: &'tcx Expr<'tcx>,
method: Symbol,
receiver: &'tcx Expr<'tcx>,
arguments: &'tcx [Expr<'tcx>],
) {
if let Some(account) = account_borrow(self.cx, expr) {
let order = self.next_order();
self.analysis.borrows.push(Borrow { order, account });
return;
}
if TARGET_METHODS.contains(&method.as_str()) {
let order = self.next_order();
self.analysis.closes.push(Close {
order,
call: expr.hir_id,
span: expr.span,
receiver: Some(receiver),
branches: self.branches.clone(),
});
return;
}
let [value] = arguments else {
return;
};
if method.as_str() != "fill" || !is_literal_zero(value) || !is_slice_fill(self.cx, expr) {
return;
}
let buffer = whole_buffer(self.cx, receiver);
let zeroed = match local_path_binding(buffer) {
Some(binding) => {
self.buffers
.get(&binding)
.map(|account| (*account, Some(binding)))
}
None => borrowed_account(self.cx, buffer).map(|account| (account, None)),
};
if let Some((account, buffer)) = zeroed {
let order = self.next_order();
self.analysis.zero_fills.push(ZeroFill {
order,
account,
buffer,
branches: self.branches.clone(),
});
}
}
fn record_lend(&mut self, path: &'tcx Expr<'tcx>) {
let mut place = path;
while let Node::Expr(parent) = self.cx.tcx.parent_hir_node(place.hir_id) {
match parent.kind {
ExprKind::Field(base, _) if base.hir_id == place.hir_id => place = parent,
ExprKind::Unary(UnOp::Deref, inner) if inner.hir_id == place.hir_id => {
place = parent
}
_ => break,
}
}
let own_close = match self_argument(self.cx, place) {
Some(SelfArgument::Trusted) => return,
Some(SelfArgument::OwnClose(close)) => Some(close),
None => None,
};
if self.lends_mutably(place) {
let order = self.next_order();
self.analysis.lends.push(Lend {
order,
lent: Lent::Place(place),
own_close,
});
}
}
fn lends_mutably(&self, place: &'tcx Expr<'tcx>) -> bool {
let typeck = self.cx.typeck_results();
let is_mutable_reference =
|| typeck.expr_ty_adjusted(place).ref_mutability() == Some(Mutability::Mut);
match self.cx.tcx.parent_hir_node(place.hir_id) {
Node::LetStmt(local) => {
let is_alias = matches!(
local.pat.kind,
rustc_hir::PatKind::Binding(mode, _, _, None) if mode.0 == ByRef::No
);
!is_alias && binds_by_mutable_reference(self.cx, local.pat)
}
Node::Expr(parent) => {
match parent.kind {
ExprKind::AddrOf(_, Mutability::Mut, _) => {
!self.alias_borrows.contains(&parent.hir_id)
}
ExprKind::AddrOf(_, Mutability::Not, _) => false,
ExprKind::Call(callee, _) if is_drop(self.cx, callee) => false,
ExprKind::Match(scrutinee, arms, _) if scrutinee.hir_id == place.hir_id => {
arms.iter()
.any(|arm| binds_by_mutable_reference(self.cx, arm.pat))
}
ExprKind::Let(binding) => binds_by_mutable_reference(self.cx, binding.pat),
_ => is_mutable_reference(),
}
}
_ => is_mutable_reference(),
}
}
fn note_projection(&mut self, expr: &Expr<'_>) {
let mut expr = expr;
while let ExprKind::Unary(UnOp::Deref, inner) = expr.kind {
expr = inner;
}
if local_path_binding(expr).is_some() {
self.projection_uses.insert(expr.hir_id);
}
}
}
impl<'tcx> Visitor<'tcx> for Collector<'_, 'tcx> {
fn visit_local(&mut self, local: &'tcx rustc_hir::LetStmt<'tcx>) {
if let rustc_hir::PatKind::Binding(mode, binding, _, None) = local.pat.kind
&& mode.0 == ByRef::No
&& let Some(initializer) = local.init
{
if is_plain_place(initializer) {
self.analysis.aliases.insert(binding, initializer);
if let ExprKind::AddrOf(_, Mutability::Mut, inner) = initializer.kind {
self.alias_borrows.insert(initializer.hir_id);
if let Some(exposed) = slot_binding(inner) {
self.exposures.push((binding, exposed));
}
}
} else if let Some(account) = borrowed_account(self.cx, initializer) {
self.buffers.insert(binding, account);
}
}
if let Some(initializer) = local.init {
self.visit_expr(initializer);
}
self.visit_pat(local.pat);
if let Some(otherwise) = local.els {
self.branches.push(otherwise.hir_id);
self.visit_block(otherwise);
self.branches.pop();
}
}
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
match expr.kind {
ExprKind::If(condition, then, otherwise) => {
self.visit_expr(condition);
self.visit_branch(then);
if let Some(otherwise) = otherwise {
self.visit_branch(otherwise);
}
}
ExprKind::Match(scrutinee, arms, source)
if !matches!(source, MatchSource::TryDesugar(_)) =>
{
self.visit_expr(scrutinee);
for arm in arms {
self.branches.push(arm.hir_id);
self.visit_arm(arm);
self.branches.pop();
}
}
ExprKind::Binary(operator, left, right)
if matches!(operator.node, BinOpKind::And | BinOpKind::Or) =>
{
self.visit_expr(left);
self.visit_branch(right);
}
ExprKind::Loop(..) | ExprKind::Block(_, Some(_)) => {
self.branches.push(expr.hir_id);
rustc_hir::intravisit::walk_expr(self, expr);
self.branches.pop();
}
_ => rustc_hir::intravisit::walk_expr(self, expr),
}
self.record(expr);
}
}
fn is_plain_place(expr: &Expr<'_>) -> bool {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner)
| ExprKind::AddrOf(_, _, inner)
| ExprKind::Field(inner, _) => is_plain_place(inner),
_ => local_path_binding(expr).is_some(),
}
}
fn place_root_binding(expr: &Expr<'_>) -> Option<HirId> {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner)
| ExprKind::AddrOf(_, _, inner)
| ExprKind::Field(inner, _) => place_root_binding(inner),
_ => local_path_binding(expr),
}
}
fn assignment_base_binding(expr: &Expr<'_>) -> Option<HirId> {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner)
| ExprKind::Field(inner, _)
| ExprKind::Index(inner, ..) => assignment_base_binding(inner),
_ => local_path_binding(expr),
}
}
fn slot_binding(expr: &Expr<'_>) -> Option<HirId> {
local_path_binding(expr)
}
fn binds_by_mutable_reference(cx: &LateContext<'_>, pattern: &Pat<'_>) -> bool {
let modes = cx.typeck_results().pat_binding_modes();
let mut binds_mutably = false;
pattern.walk_always(|pattern| {
binds_mutably |= modes
.get(pattern.hir_id)
.is_some_and(|mode| matches!(mode.0, ByRef::Yes(_, Mutability::Mut)));
});
binds_mutably
}
fn is_trusted_method(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
cx.typeck_results()
.type_dependent_def_id(expr.hir_id)
.is_some_and(|definition| is_trusted_crate(cx.tcx.crate_name(definition.krate).as_str()))
}
fn local_path_binding(expr: &Expr<'_>) -> Option<HirId> {
let ExprKind::Path(rustc_hir::QPath::Resolved(_, path)) = expr.kind else {
return None;
};
let rustc_hir::def::Res::Local(binding) = path.res else {
return None;
};
Some(binding)
}
fn account_borrow<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
let (definition, account) = match expr.kind {
ExprKind::MethodCall(_, account, [], _) => {
(
cx.typeck_results().type_dependent_def_id(expr.hir_id)?,
account,
)
}
ExprKind::Call(callee, [account]) => (callee_definition(cx, callee)?, account),
_ => return None,
};
(cx.tcx.crate_name(definition.krate).as_str() == "solana_account_view"
&& cx.tcx.item_name(definition).as_str() == "try_borrow_mut")
.then_some(account)
}
fn callee_definition(cx: &LateContext<'_>, callee: &Expr<'_>) -> Option<rustc_hir::def_id::DefId> {
let ExprKind::Path(ref path) = callee.kind else {
return None;
};
let rustc_hir::def::Res::Def(_, definition) = cx.qpath_res(path, callee.hir_id) else {
return None;
};
Some(definition)
}
fn callee_name(callee: &Expr<'_>) -> Option<Symbol> {
match callee.kind {
ExprKind::Path(rustc_hir::QPath::Resolved(_, path)) => {
path.segments.last().map(|segment| segment.ident.name)
}
ExprKind::Path(rustc_hir::QPath::TypeRelative(_, segment)) => Some(segment.ident.name),
_ => None,
}
}
enum SelfArgument {
Trusted,
OwnClose(HirId),
}
fn self_argument(cx: &LateContext<'_>, place: &Expr<'_>) -> Option<SelfArgument> {
let argument = match cx.tcx.parent_hir_node(place.hir_id) {
Node::Expr(parent) if matches!(parent.kind, ExprKind::AddrOf(..)) => parent,
_ => place,
};
let Node::Expr(call) = cx.tcx.parent_hir_node(argument.hir_id) else {
return None;
};
let (is_trusted, name) = match call.kind {
ExprKind::MethodCall(segment, receiver, ..) if receiver.hir_id == argument.hir_id => {
(is_trusted_method(cx, call), segment.ident.name)
}
ExprKind::Call(callee, [first, ..]) if first.hir_id == argument.hir_id => {
let is_trusted = callee_definition(cx, callee).is_some_and(|definition| {
is_trusted_crate(cx.tcx.crate_name(definition.krate).as_str())
});
(is_trusted, callee_name(callee)?)
}
_ => return None,
};
if is_trusted {
Some(SelfArgument::Trusted)
} else if TARGET_METHODS.contains(&name.as_str()) {
Some(SelfArgument::OwnClose(call.hir_id))
} else {
None
}
}
fn is_trusted_crate(name: &str) -> bool {
matches!(name, "solana_account_view" | "pinocchio" | "pina")
}
fn borrowed_account<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
let ExprKind::Match(scrutinee, _, MatchSource::TryDesugar(_)) = expr.kind else {
return None;
};
account_borrow(cx, shared::try_branch_argument(cx, scrutinee)?)
}
fn whole_buffer<'tcx>(cx: &LateContext<'tcx>, mut expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
loop {
match expr.kind {
ExprKind::Unary(UnOp::Deref, inner) => expr = inner,
ExprKind::Index(inner, index, _) if is_range_full(cx, index) => expr = inner,
_ => return expr,
}
}
}
fn is_range_full(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
cx.typeck_results()
.expr_ty(expr)
.ty_adt_def()
.is_some_and(|definition| cx.tcx.is_lang_item(definition.did(), LangItem::RangeFull))
}
fn is_slice_fill(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
cx.typeck_results()
.type_dependent_def_id(expr.hir_id)
.is_some_and(|definition| {
cx.tcx.crate_name(definition.krate).as_str() == "core"
&& cx.tcx.item_name(definition).as_str() == "fill"
})
}
fn is_literal_zero(expr: &Expr<'_>) -> bool {
let ExprKind::Lit(literal) = expr.kind else {
return false;
};
matches!(literal.node, LitKind::Int(value, _) if value.get() == 0)
}
fn is_drop(cx: &LateContext<'_>, callee: &Expr<'_>) -> bool {
callee_definition(cx, callee).is_some_and(|definition| {
matches!(
cx.tcx.def_path_str(definition).as_str(),
"std::mem::drop" | "core::mem::drop"
)
})
}