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::LitKind;
use rustc_hir::BinOpKind;
use rustc_hir::Block;
use rustc_hir::Body;
use rustc_hir::Expr;
use rustc_hir::ExprKind;
use rustc_hir::HirId;
use rustc_hir::LetStmt;
use rustc_hir::MatchSource;
use rustc_hir::Node;
use rustc_hir::Pat;
use rustc_hir::PatExprKind;
use rustc_hir::PatKind;
use rustc_hir::QPath;
use rustc_hir::Stmt;
use rustc_hir::StmtKind;
use rustc_hir::UnOp;
use rustc_hir::def::CtorOf;
use rustc_hir::def::DefKind;
use rustc_hir::def::Res;
use rustc_hir::def_id::DefId;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::intravisit::FnKind;
use rustc_hir::intravisit::Visitor;
use rustc_lint::LateContext;
use rustc_lint::LateLintPass;
use rustc_middle::ty::EarlyBinder;
use rustc_middle::ty::GenericArgsRef;
use rustc_middle::ty::Instance;
use rustc_middle::ty::InstanceKind;
use rustc_middle::ty::TyKind;
use rustc_middle::ty::TypeckResults;
use rustc_middle::ty::TypingEnv;
use rustc_middle::ty::Unnormalized;
use rustc_span::ExpnKind;
use rustc_span::MacroKind;
use rustc_span::Span;
use crate::diagnostics;
use crate::shared;
crate::declare_late_lint! {
pub REQUIRE_GUARDED_FULL_BALANCE_DRAIN,
Warn,
"full-balance drains should be gated by a pause or circuit-breaker guard"
}
const DRAIN_METHODS: &[&str] = &["send", "send_owned"];
const CLOSE_METHODS: &[&str] = &[
"zeroed",
"close",
"close_with_recipient",
"close_account_zeroed",
];
const GUARD_TERMS: &[&str] = &[
"pause", "cap", "circuit", "halt", "guard", "limit", "throttle",
];
const TARGET_NEEDLES: &[&str] = &["process", "process_instruction", "instruction"];
const FAILURE_PRESERVING_METHODS: &[&str] = &[
"and",
"and_then",
"inspect",
"inspect_err",
"map",
"map_err",
"ok",
"ok_or",
"ok_or_else",
];
const FAILURE_ABORTING_METHODS: &[&str] = &["expect", "unwrap"];
const FAILURE_TRUE_PREDICATES: &[&str] = &["is_err", "is_none"];
const FAILURE_FALSE_PREDICATES: &[&str] = &["is_ok", "is_ok_and", "is_some", "is_some_and"];
const ASSERT_EQUALITY_MACROS: &[&str] = &["assert_eq", "assert_ne"];
const MAX_WRAPPER_DEPTH: usize = 3;
#[derive(Debug, Clone, Copy)]
struct DrainFacts {
full_balance: bool,
guarded: bool,
closing: bool,
}
#[derive(Debug, Clone)]
struct SeenGuard {
scopes: Vec<u32>,
receiver: Option<String>,
is_close: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GuardValue {
Fallible,
Flag(Option<bool>),
}
#[derive(Debug)]
enum Enforcement {
Enforced(Vec<HirId>),
Bound(HirId, GuardValue),
Unenforced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Exit {
Failure,
Escape,
FallThrough,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PatternClass {
Success,
Failure,
Both,
}
struct DrainAnalyzer<'a, 'tcx> {
cx: &'a LateContext<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
body_value: HirId,
callers: Vec<LocalDefId>,
instance_args: Option<GenericArgsRef<'tcx>>,
parameters: HashSet<HirId>,
origins: HashMap<HirId, &'tcx Expr<'tcx>>,
pending: HashMap<HirId, GuardValue>,
short_circuit_depths: HashMap<HirId, usize>,
scopes: Vec<u32>,
next_scope: u32,
guards: Vec<SeenGuard>,
escapes: Vec<usize>,
initializers: HashMap<HirId, &'tcx Expr<'tcx>>,
drains: HashMap<Span, DrainFacts>,
order: Vec<Span>,
}
impl<'a, 'tcx> DrainAnalyzer<'a, 'tcx> {
fn analyze(
cx: &'a LateContext<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
body: &'tcx Body<'tcx>,
callers: Vec<LocalDefId>,
instance_args: Option<GenericArgsRef<'tcx>>,
) -> Self {
let mut parameters = HashSet::new();
for parameter in body.params {
parameter.pat.each_binding(|_, binding, _, _| {
parameters.insert(binding);
});
}
let mut analyzer = Self {
cx,
typeck,
body_value: body.value.hir_id,
callers,
instance_args,
parameters,
origins: HashMap::new(),
pending: HashMap::new(),
short_circuit_depths: HashMap::new(),
scopes: Vec::new(),
next_scope: 0,
guards: Vec::new(),
escapes: Vec::new(),
initializers: HashMap::new(),
drains: HashMap::new(),
order: Vec::new(),
};
analyzer.visit_expr(body.value);
analyzer
}
fn root(&self) -> LocalDefId {
*self
.callers
.first()
.expect("an analysis always starts from its own body")
}
fn is_handler(&self) -> bool {
self.callers.len() == 1
}
fn has_dominant(&self, keep: impl Fn(&SeenGuard) -> bool) -> bool {
self.guards
.iter()
.any(|guard| keep(guard) && self.scopes.starts_with(&guard.scopes))
}
fn guards_every_exit(&self) -> bool {
let first_escape = self.escapes.first().copied().unwrap_or(usize::MAX);
self.guards
.iter()
.take(first_escape)
.any(|guard| !guard.is_close && guard.scopes.is_empty())
}
fn resolves_to_full_balance(&self, expr: &Expr<'_>, receiver: &str) -> bool {
match &expr.kind {
ExprKind::MethodCall(segment, inner_receiver, arguments, _) => {
segment.ident.name.as_str() == "lamports"
&& arguments.is_empty()
&& shared::expression_identity(inner_receiver).as_deref() == Some(receiver)
}
ExprKind::Path(QPath::Resolved(_, path)) => {
match path.res {
Res::Local(binding) => {
self.initializers.get(&binding).is_some_and(|initializer| {
self.resolves_to_full_balance(initializer, receiver)
})
}
_ => false,
}
}
ExprKind::DropTemps(inner)
| ExprKind::Use(inner, _)
| ExprKind::AddrOf(_, _, inner)
| ExprKind::Unary(_, inner) => self.resolves_to_full_balance(inner, receiver),
_ => false,
}
}
fn record_call(
&mut self,
call: &'tcx Expr<'tcx>,
name: &str,
receiver: Option<String>,
inputs: &[&'tcx Expr<'tcx>],
callee: Option<(DefId, HirId)>,
) {
if CLOSE_METHODS.contains(&name) && receiver.is_some() {
self.guards.push(SeenGuard {
scopes: self.scopes.clone(),
receiver,
is_close: true,
});
return;
}
if !inputs.iter().any(|input| self.reads_parameter(input)) {
return;
}
let implementation = callee.and_then(|(definition, args)| self.resolve(definition, args));
if implementation.is_none()
&& !self.is_handler()
&& callee
.is_some_and(|(definition, _)| self.cx.tcx.trait_of_assoc(definition).is_some())
{
return;
}
let local_body = implementation.and_then(|(definition, args)| {
self.local_body(definition)
.map(|(local, body)| (local, body, args))
});
let Some(value) = self.guard_value(call) else {
if names_guard(name)
&& self.typeck.expr_ty(call).is_unit()
&& local_body.is_some_and(|(local, body, _)| body_can_panic(self.cx, local, body))
{
self.guards.push(SeenGuard {
scopes: self.scopes.clone(),
receiver: None,
is_close: false,
});
}
return;
};
if let Some((local, body, _)) = local_body
&& returns_constant(self.cx, local, body, value, Outcome::Succeeds)
{
return;
}
let delegates = value == GuardValue::Fallible
&& local_body
.is_some_and(|(local, body, args)| self.is_guard_wrapper(local, body, args));
if names_guard(name) || delegates {
self.enforce(call, value);
}
}
fn enforce(&mut self, expr: &'tcx Expr<'tcx>, value: GuardValue) {
match self.enforcement(expr, value) {
Enforcement::Enforced(short_circuits) => {
let depth = short_circuits
.iter()
.filter_map(|operator| self.short_circuit_depths.get(operator))
.min()
.copied()
.unwrap_or(self.scopes.len());
self.guards.push(SeenGuard {
scopes: self.scopes[..depth].to_vec(),
receiver: None,
is_close: false,
});
}
Enforcement::Bound(binding, value) => {
self.pending.insert(binding, value);
}
Enforcement::Unenforced => {}
}
}
fn guard_value(&self, call: &Expr<'_>) -> Option<GuardValue> {
let ty = self.typeck.expr_ty(call);
if ty.is_bool() {
return Some(GuardValue::Flag(None));
}
let TyKind::Adt(definition, _) = ty.kind() else {
return None;
};
is_core_item(self.cx, definition.did(), &["Result", "Option"])
.then_some(GuardValue::Fallible)
}
fn resolve(
&self,
definition: DefId,
args_owner: HirId,
) -> Option<(DefId, GenericArgsRef<'tcx>)> {
let tcx = self.cx.tcx;
if !matches!(tcx.def_kind(definition), DefKind::Fn | DefKind::AssocFn) {
return None;
}
let args = self.typeck.node_args(args_owner);
if args.len() != tcx.generics_of(definition).count() {
return None;
}
let args = match self.instance_args {
Some(instance_args) => EarlyBinder::bind(tcx, args).instantiate(tcx, instance_args),
None => Unnormalized::new_wip(args),
};
let typing_env = TypingEnv::post_analysis(tcx, self.root().to_def_id());
let Ok(args) = tcx.try_normalize_erasing_regions(typing_env, args) else {
return None;
};
match Instance::try_resolve(tcx, typing_env, definition, args) {
Ok(Some(instance)) => {
match instance.def {
InstanceKind::Item(item) => Some((item, instance.args)),
_ => None,
}
}
_ => None,
}
}
fn local_body(&self, definition: DefId) -> Option<(LocalDefId, &'tcx Body<'tcx>)> {
let local = definition.as_local()?;
let body = self.cx.tcx.hir_maybe_body_owned_by(local)?;
Some((local, body))
}
fn is_guard_wrapper(
&self,
local: LocalDefId,
body: &'tcx Body<'tcx>,
args: GenericArgsRef<'tcx>,
) -> bool {
if self.callers.len() > MAX_WRAPPER_DEPTH
|| self.callers.contains(&local)
|| args.len() != self.cx.tcx.generics_of(local).count()
{
return false;
}
let mut callers = self.callers.clone();
callers.push(local);
DrainAnalyzer::analyze(
self.cx,
self.cx.tcx.typeck(local),
body,
callers,
Some(args),
)
.guards_every_exit()
}
fn reads_parameter(&self, expr: &'tcx Expr<'tcx>) -> bool {
let mut seen = HashSet::new();
locals_in(expr)
.into_iter()
.any(|local| self.local_from_parameter(local, &mut seen))
}
fn local_from_parameter(&self, local: HirId, seen: &mut HashSet<HirId>) -> bool {
if self.parameters.contains(&local) {
return true;
}
if !seen.insert(local) {
return false;
}
self.origins.get(&local).is_some_and(|origin| {
locals_in(origin)
.into_iter()
.any(|inner| self.local_from_parameter(inner, seen))
})
}
fn bind_pattern(&mut self, pattern: &'tcx Pat<'tcx>, origin: &'tcx Expr<'tcx>) {
pattern.each_binding(|_, binding, _, _| {
self.origins.insert(binding, origin);
});
}
fn enforcement(&self, start: &Expr<'_>, mut value: GuardValue) -> Enforcement {
let tcx = self.cx.tcx;
let mut child = start.hir_id;
let mut short_circuits = Vec::new();
loop {
if child == self.body_value {
return returned(value, short_circuits);
}
let parent = match tcx.parent_hir_node(child) {
Node::Expr(parent) => parent,
Node::Block(block) if block.expr.is_some_and(|tail| tail.hir_id == child) => {
child = block.hir_id;
continue;
}
Node::LetStmt(local) if local.init.is_some_and(|init| init.hir_id == child) => {
return self.let_statement(local, value, short_circuits);
}
_ => return Enforcement::Unenforced,
};
match &parent.kind {
ExprKind::Call(callee, arguments) => {
let is_first = arguments.first().is_some_and(|first| first.hir_id == child);
if is_first && arguments.len() == 1 && is_try_branch(tcx, parent) {
return match value {
GuardValue::Fallible => Enforcement::Enforced(short_circuits),
GuardValue::Flag(_) => Enforcement::Unenforced,
};
}
if is_first
&& matches!(value, GuardValue::Flag(None | Some(false)))
&& self.is_pina_assert(callee)
{
value = GuardValue::Fallible;
} else if !(is_first
&& arguments.len() == 1
&& value == GuardValue::Fallible
&& self.is_fallible_from(callee, parent, &arguments[0]))
{
return Enforcement::Unenforced;
}
}
ExprKind::MethodCall(segment, receiver, arguments, _)
if receiver.hir_id == child && value == GuardValue::Fallible =>
{
let method = segment.ident.name.as_str();
if !self.is_core_fallible_method(parent) {
let Some(adapted) =
self.core_trait_adapter(parent, receiver, method, arguments)
else {
return Enforcement::Unenforced;
};
value = adapted;
child = parent.hir_id;
continue;
}
if FAILURE_ABORTING_METHODS.contains(&method) {
return Enforcement::Enforced(short_circuits);
}
if FAILURE_TRUE_PREDICATES.contains(&method) {
value = GuardValue::Flag(Some(true));
} else if FAILURE_FALSE_PREDICATES.contains(&method) {
value = GuardValue::Flag(Some(false));
} else if method == "or"
&& arguments
.first()
.is_some_and(|fallback| self.is_variant(fallback, &["Err", "None"]))
{
} else if method == "or_else"
&& arguments
.first()
.is_some_and(|fallback| self.always_fails(fallback))
{
} else if !FAILURE_PRESERVING_METHODS.contains(&method) {
return Enforcement::Unenforced;
}
}
ExprKind::Unary(UnOp::Not, _) => {
let GuardValue::Flag(polarity) = value else {
return Enforcement::Unenforced;
};
value = GuardValue::Flag(polarity.map(|failing| !failing));
}
ExprKind::Binary(operator, left, right) => {
let other = if left.hir_id == child { right } else { left };
if value == GuardValue::Fallible {
if !matches!(operator.node, BinOpKind::Eq | BinOpKind::Ne)
|| !self.is_variant(peel_reference(other), &["Ok", "Some"])
{
return Enforcement::Unenforced;
}
value = GuardValue::Flag(Some(operator.node == BinOpKind::Ne));
child = parent.hir_id;
continue;
}
let GuardValue::Flag(polarity) = value else {
return Enforcement::Unenforced;
};
value = match operator.node {
BinOpKind::Or if polarity != Some(false) => GuardValue::Flag(Some(true)),
BinOpKind::And if polarity != Some(true) => GuardValue::Flag(Some(false)),
BinOpKind::Eq | BinOpKind::Ne => {
let Some(literal) = bool_literal(other) else {
return Enforcement::Unenforced;
};
let flips = (!literal) != (operator.node == BinOpKind::Ne);
GuardValue::Flag(polarity.map(|failing| failing != flips))
}
_ => return Enforcement::Unenforced,
};
if matches!(operator.node, BinOpKind::And | BinOpKind::Or)
&& right.hir_id == child
{
short_circuits.push(parent.hir_id);
}
}
ExprKind::If(condition, then, otherwise) if condition.hir_id == child => {
let GuardValue::Flag(polarity) = value else {
return Enforcement::Unenforced;
};
let then_fails = self.branch_fails(then, parent);
let else_fails =
otherwise.is_some_and(|otherwise| self.branch_fails(otherwise, parent));
let enforced = match polarity {
Some(true) => then_fails,
Some(false) => else_fails,
None => then_fails || else_fails,
};
return enforced_if(enforced, short_circuits);
}
ExprKind::Let(let_expr) if let_expr.init.hir_id == child => {
if value != GuardValue::Fallible {
return Enforcement::Unenforced;
}
return enforced_if(
self.if_let_enforces(parent.hir_id, let_expr.pat),
short_circuits,
);
}
ExprKind::Match(scrutinee, arms, MatchSource::Normal)
if scrutinee.hir_id == child && value == GuardValue::Fallible =>
{
return enforced_if(self.arms_fail(arms, parent), short_circuits);
}
ExprKind::Ret(Some(returned_value)) if returned_value.hir_id == child => {
return returned(value, short_circuits);
}
ExprKind::AddrOf(_, _, inner) if inner.hir_id == child => {
return enforced_if(self.asserted_equal(parent, value), short_circuits);
}
ExprKind::Block(_, Some(_)) if self.breaks_without_failure(parent) => {
return Enforcement::Unenforced;
}
ExprKind::Block(..)
| ExprKind::DropTemps(_)
| ExprKind::Use(..)
| ExprKind::Type(..) => {}
_ => return Enforcement::Unenforced,
}
child = parent.hir_id;
}
}
fn branch_fails(&self, branch: &Expr<'_>, conditional: &Expr<'_>) -> bool {
self.exit(branch) == Exit::Failure
|| (self.yields_failure(branch)
&& matches!(
self.enforcement(conditional, GuardValue::Fallible),
Enforcement::Enforced(_)
))
}
fn yields_failure(&self, expr: &Expr<'_>) -> bool {
match &expr.kind {
ExprKind::Block(block, _) => {
block.stmts.iter().all(|statement| {
match &statement.kind {
StmtKind::Expr(inner) | StmtKind::Semi(inner) => {
self.exit(inner) == Exit::FallThrough
}
StmtKind::Let(local) => {
local.els.is_none()
&& local
.init
.is_none_or(|init| self.exit(init) == Exit::FallThrough)
}
StmtKind::Item(_) => true,
}
}) && block.expr.is_some_and(|tail| self.yields_failure(tail))
}
_ => self.is_variant(expr, &["Err", "None"]),
}
}
fn arms_fail(&self, arms: &[rustc_hir::Arm<'_>], matched: &Expr<'_>) -> bool {
let mut success_left = true;
let mut failure_left = true;
for arm in arms {
let class = classify_pattern(arm.pat);
let receives_failure = failure_left && class != PatternClass::Success;
if receives_failure {
let only_failure = class == PatternClass::Failure || !success_left;
let propagates = only_failure && self.returns_own_binding(arm);
if !propagates && !self.branch_fails(arm.body, matched) {
return false;
}
}
if arm.guard.is_none() && covers_class(arm.pat) {
match class {
PatternClass::Success => success_left = false,
PatternClass::Failure => failure_left = false,
PatternClass::Both => return true,
}
}
}
true
}
fn returns_own_binding(&self, arm: &rustc_hir::Arm<'_>) -> bool {
let PatKind::Binding(_, binding, ..) = arm.pat.kind else {
return false;
};
let is_binding = |expr: &Expr<'_>| {
matches!(
&expr.kind,
ExprKind::Path(QPath::Resolved(_, path)) if path.res == Res::Local(binding)
)
};
let mut body = arm.body;
loop {
match &body.kind {
ExprKind::Ret(Some(value)) => return is_binding(value),
ExprKind::Block(block, None) if block.stmts.is_empty() => {
let Some(tail) = block.expr else {
return false;
};
body = tail;
}
ExprKind::Block(block, None) => {
let [rest @ .., last] = block.stmts else {
return false;
};
let leaves_early = rest.iter().any(|statement| {
matches!(
&statement.kind,
StmtKind::Expr(inner) | StmtKind::Semi(inner)
if self.exit(inner) != Exit::FallThrough
)
});
if leaves_early || block.expr.is_some() {
return false;
}
let (StmtKind::Expr(inner) | StmtKind::Semi(inner)) = &last.kind else {
return false;
};
body = inner;
}
_ => return false,
}
}
}
fn returns_failure(&self, value: &Expr<'_>, lenient: bool) -> bool {
if let Some(variant) = variant_constructed(self.cx, self.typeck, value) {
return matches!(variant.as_str(), "Err" | "None");
}
let callee = match &value.kind {
ExprKind::Call(callee, _) => {
match &callee.kind {
ExprKind::Path(qpath) => {
match self.typeck.qpath_res(qpath, callee.hir_id) {
Res::Def(DefKind::Fn | DefKind::AssocFn, definition) => {
Some((definition, callee.hir_id))
}
_ => None,
}
}
_ => None,
}
}
ExprKind::MethodCall(..) => {
self.typeck
.type_dependent_def_id(value.hir_id)
.map(|definition| (definition, value.hir_id))
}
_ => return false,
};
let Some((definition, args_owner)) = callee else {
return false;
};
let local_body = self
.resolve(definition, args_owner)
.and_then(|(implementation, _)| self.local_body(implementation));
match local_body {
Some((local, body)) => {
returns_constant(self.cx, local, body, GuardValue::Fallible, Outcome::Fails)
}
None => lenient,
}
}
fn always_fails(&self, fallback: &Expr<'_>) -> bool {
match &fallback.kind {
ExprKind::Closure(closure) => {
let body = self.cx.tcx.hir_body(closure.body);
returns_constant(
self.cx,
closure.def_id,
body,
GuardValue::Fallible,
Outcome::Fails,
)
}
_ => self.is_variant(fallback, &["Err", "None"]),
}
}
fn breaks_without_failure(&self, block: &Expr<'_>) -> bool {
breaks_to(block).any(|value| !value.is_some_and(|value| self.returns_failure(value, false)))
}
fn let_statement(
&self,
local: &'tcx LetStmt<'tcx>,
value: GuardValue,
short_circuits: Vec<HirId>,
) -> Enforcement {
if let Some(otherwise) = local.els {
let enforced = value == GuardValue::Fallible
&& classify_pattern(local.pat) == PatternClass::Success
&& self.block_exit(otherwise) == Exit::Failure;
return enforced_if(enforced, short_circuits);
}
match local.pat.kind {
PatKind::Binding(_, binding, _, None) => Enforcement::Bound(binding, value),
_ => Enforcement::Unenforced,
}
}
fn if_let_enforces(&self, let_id: HirId, pattern: &Pat<'_>) -> bool {
let tcx = self.cx.tcx;
let mut child = let_id;
loop {
let Node::Expr(parent) = tcx.parent_hir_node(child) else {
return false;
};
match &parent.kind {
ExprKind::DropTemps(_) => child = parent.hir_id,
ExprKind::If(condition, then, otherwise) if condition.hir_id == child => {
return match classify_pattern(pattern) {
PatternClass::Failure => self.branch_fails(then, parent),
PatternClass::Success => {
otherwise.is_some_and(|otherwise| self.branch_fails(otherwise, parent))
}
PatternClass::Both => false,
};
}
_ => return false,
}
}
}
fn asserted_equal(&self, reference: &Expr<'_>, value: GuardValue) -> bool {
let tcx = self.cx.tcx;
let Node::Expr(tuple) = tcx.parent_hir_node(reference.hir_id) else {
return false;
};
let ExprKind::Tup([left, right]) = &tuple.kind else {
return false;
};
let Node::Expr(matched) = tcx.parent_hir_node(tuple.hir_id) else {
return false;
};
let ExprKind::Match(scrutinee, ..) = &matched.kind else {
return false;
};
let Some(assertion) = bang_macro_name(matched.span) else {
return false;
};
if scrutinee.hir_id != tuple.hir_id || !ASSERT_EQUALITY_MACROS.contains(&assertion.as_str())
{
return false;
}
let other = peel_reference(if left.hir_id == reference.hir_id {
right
} else {
left
});
let is_equality = assertion.as_str() == "assert_eq";
match value {
GuardValue::Flag(polarity) => {
bool_literal(other).is_some_and(|literal| {
let continuing = if is_equality { literal } else { !literal };
polarity.is_none_or(|failing| continuing != failing)
})
}
GuardValue::Fallible => is_equality && self.is_variant(other, &["Ok", "Some"]),
}
}
fn is_pina_assert(&self, callee: &Expr<'_>) -> bool {
let ExprKind::Path(qpath) = &callee.kind else {
return false;
};
let Res::Def(DefKind::Fn, definition) = self.typeck.qpath_res(qpath, callee.hir_id) else {
return false;
};
let tcx = self.cx.tcx;
tcx.crate_name(definition.krate).as_str() == "pina"
&& matches!(
tcx.def_path_str(definition).as_str(),
"pina::assert" | "pina::utils::assert"
)
}
fn is_core_fallible_method(&self, call: &Expr<'_>) -> bool {
let Some(definition) = self.typeck.type_dependent_def_id(call.hir_id) else {
return false;
};
let tcx = self.cx.tcx;
let Some(implementation) = tcx.inherent_impl_of_assoc(definition) else {
return false;
};
let TyKind::Adt(owner, _) = tcx
.type_of(implementation)
.instantiate_identity()
.skip_norm_wip()
.kind()
else {
return false;
};
is_core_item(self.cx, owner.did(), &["Result", "Option"])
}
fn core_trait_of(&self, definition: DefId) -> Option<rustc_span::Symbol> {
let tcx = self.cx.tcx;
let owner = tcx.trait_of_assoc(definition)?;
(tcx.crate_name(owner.krate).as_str() == "core").then(|| tcx.item_name(owner))
}
fn core_trait_adapter(
&self,
call: &Expr<'_>,
receiver: &Expr<'_>,
method: &str,
arguments: &[Expr<'_>],
) -> Option<GuardValue> {
let definition = self.typeck.type_dependent_def_id(call.hir_id)?;
let owner = self.core_trait_of(definition)?;
match (owner.as_str(), method) {
("Clone", "clone") | ("Into", "into") if self.keeps_failure(receiver, call) => {
Some(GuardValue::Fallible)
}
("PartialEq", "eq" | "ne") => {
let [other] = arguments else {
return None;
};
let other = peel_reference(other);
if !self.is_variant(other, &["Ok", "Some"]) {
return None;
}
Some(GuardValue::Flag(Some(method == "ne")))
}
_ => None,
}
}
fn keeps_failure(&self, input: &Expr<'_>, output: &Expr<'_>) -> bool {
let output_ty = self.typeck.expr_ty(output);
output_ty == self.typeck.expr_ty(input)
|| matches!(
output_ty.kind(),
TyKind::Adt(definition, _) if is_core_item(self.cx, definition.did(), &["Result"])
)
}
fn is_fallible_from(&self, callee: &Expr<'_>, call: &Expr<'_>, input: &Expr<'_>) -> bool {
let ExprKind::Path(qpath) = &callee.kind else {
return false;
};
let Res::Def(DefKind::AssocFn, definition) = self.typeck.qpath_res(qpath, callee.hir_id)
else {
return false;
};
self.core_trait_of(definition)
.is_some_and(|owner| owner.as_str() == "From")
&& self.keeps_failure(input, call)
}
fn is_variant(&self, expr: &Expr<'_>, variants: &[&str]) -> bool {
variant_constructed(self.cx, self.typeck, expr)
.is_some_and(|variant| variants.contains(&variant.as_str()))
}
fn exit(&self, expr: &Expr<'_>) -> Exit {
match &expr.kind {
ExprKind::Ret(Some(value)) => {
if self.returns_failure(value, self.is_handler()) {
Exit::Failure
} else {
Exit::Escape
}
}
ExprKind::Ret(None) | ExprKind::Break(..) | ExprKind::Continue(_) => Exit::Escape,
ExprKind::Block(block, _) => self.block_exit(block),
ExprKind::If(condition, then, otherwise) => {
let condition = self.exit(condition);
if condition != Exit::FallThrough {
return condition;
}
let otherwise =
otherwise.map_or(Exit::FallThrough, |otherwise| self.exit(otherwise));
combine_branches([self.exit(then), otherwise])
}
ExprKind::Match(scrutinee, arms, MatchSource::TryDesugar(_)) => {
let fails = match &scrutinee.kind {
ExprKind::Call(_, [argument]) => self.is_variant(argument, &["Err", "None"]),
_ => false,
};
let _ = arms;
if fails {
Exit::Failure
} else {
Exit::FallThrough
}
}
ExprKind::Match(scrutinee, arms, _) => {
let scrutinee = self.exit(scrutinee);
if scrutinee != Exit::FallThrough {
return scrutinee;
}
combine_branches(arms.iter().map(|arm| self.exit(arm.body)))
}
ExprKind::Call(..) | ExprKind::MethodCall(..)
if self.typeck.expr_ty(expr).is_never() =>
{
Exit::Failure
}
ExprKind::DropTemps(inner) | ExprKind::Use(inner, _) | ExprKind::Type(inner, _) => {
self.exit(inner)
}
_ => Exit::FallThrough,
}
}
fn block_exit(&self, block: &Block<'_>) -> Exit {
for statement in block.stmts {
let exit = match &statement.kind {
StmtKind::Expr(expr) | StmtKind::Semi(expr) => self.exit(expr),
StmtKind::Let(local) => {
let initializer = local.init.map_or(Exit::FallThrough, |init| self.exit(init));
match (initializer, local.els) {
(Exit::FallThrough, Some(otherwise))
if self.block_exit(otherwise) == Exit::Escape =>
{
Exit::Escape
}
(exit, _) => exit,
}
}
StmtKind::Item(_) => Exit::FallThrough,
};
if exit != Exit::FallThrough {
return exit;
}
}
block.expr.map_or(Exit::FallThrough, |tail| self.exit(tail))
}
fn visit_branch(&mut self, expr: &'tcx Expr<'tcx>) {
let id = self.next_scope;
self.next_scope += 1;
self.scopes.push(id);
self.visit_expr(expr);
self.scopes.pop();
}
fn visit_branch_block(&mut self, block: &'tcx Block<'tcx>) {
let id = self.next_scope;
self.next_scope += 1;
self.scopes.push(id);
self.visit_block(block);
self.scopes.pop();
}
fn visit_block(&mut self, block: &'tcx Block<'tcx>) {
for statement in block.stmts {
self.visit_stmt(statement);
}
if let Some(tail) = block.expr {
self.visit_expr(tail);
}
}
fn visit_stmt(&mut self, statement: &'tcx Stmt<'tcx>) {
if let StmtKind::Let(local) = &statement.kind {
if let (PatKind::Binding(_, binding, ..), Some(initializer)) =
(&local.pat.kind, local.init)
{
self.initializers.insert(*binding, initializer);
}
if let Some(initializer) = local.init {
self.visit_expr(initializer);
self.bind_pattern(local.pat, initializer);
}
if let Some(else_block) = local.els {
self.visit_branch_block(else_block);
}
} else if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = &statement.kind {
self.visit_expr(expr);
}
}
fn forget_pending(&mut self, place: &Expr<'_>) {
if let Some(binding) = shared::expression_local_binding(place) {
self.pending.remove(&binding);
}
}
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
match &expr.kind {
ExprKind::MethodCall(segment, receiver, arguments, _) => {
let method = segment.ident.name.as_str();
let receiver_identity = shared::expression_identity(receiver);
if DRAIN_METHODS.contains(&method) && arguments.len() == 3 {
let full_balance = receiver_identity.as_deref().is_some_and(|receiver| {
arguments
.get(1)
.is_some_and(|amount| self.resolves_to_full_balance(amount, receiver))
});
let guarded = self.has_dominant(|guard| !guard.is_close);
let closing = self.has_dominant(|guard| {
guard.is_close
&& guard.receiver.is_some()
&& guard.receiver == receiver_identity
});
self.drains.insert(
expr.span,
DrainFacts {
full_balance,
guarded,
closing,
},
);
self.order.push(expr.span);
} else {
let inputs: Vec<&'tcx Expr<'tcx>> =
std::iter::once(*receiver).chain(arguments.iter()).collect();
let callee = self
.typeck
.type_dependent_def_id(expr.hir_id)
.map(|definition| (definition, expr.hir_id));
self.record_call(expr, method, receiver_identity, &inputs, callee);
}
self.visit_expr(receiver);
for argument in *arguments {
self.visit_expr(argument);
}
}
ExprKind::Path(QPath::Resolved(_, path)) => {
if let Res::Local(binding) = path.res
&& let Some(value) = self.pending.get(&binding).copied()
{
self.enforce(expr, value);
}
}
ExprKind::Block(block, Some(_)) if breaks_to(expr).next().is_some() => {
self.visit_branch_block(block);
}
ExprKind::Block(block, _) => self.visit_block(block),
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, _, MatchSource::TryDesugar(_)) => self.visit_expr(scrutinee),
ExprKind::Match(scrutinee, arms, _) => {
self.visit_expr(scrutinee);
for arm in *arms {
self.bind_pattern(arm.pat, scrutinee);
if let Some(guard) = arm.guard {
self.visit_branch(guard);
}
self.visit_branch(arm.body);
}
}
ExprKind::Loop(block, ..) => self.visit_branch_block(block),
ExprKind::Closure(_) => {}
ExprKind::Call(callee, arguments) => {
if let ExprKind::Path(qpath) = &callee.kind
&& let Some(name) = qpath_name(qpath)
{
if let Res::Def(DefKind::Fn | DefKind::AssocFn, definition) =
self.typeck.qpath_res(qpath, callee.hir_id)
{
let inputs: Vec<&'tcx Expr<'tcx>> = arguments.iter().collect();
self.record_call(
expr,
name,
None,
&inputs,
Some((definition, callee.hir_id)),
);
}
}
self.visit_expr(callee);
for argument in *arguments {
self.visit_expr(argument);
}
}
ExprKind::Binary(operator, left, right) => {
self.visit_expr(left);
if matches!(operator.node, BinOpKind::And | BinOpKind::Or) {
self.short_circuit_depths
.insert(expr.hir_id, self.scopes.len());
self.visit_branch(right);
} else {
self.visit_expr(right);
}
}
ExprKind::Assign(left, right, _) | ExprKind::AssignOp(_, left, right) => {
self.visit_expr(left);
self.visit_expr(right);
self.forget_pending(left);
}
ExprKind::AddrOf(_, mutability, inner) => {
self.visit_expr(inner);
if mutability.is_mut() {
self.forget_pending(inner);
}
}
ExprKind::Index(base, index, _) => {
self.visit_expr(base);
self.visit_expr(index);
}
ExprKind::Let(let_expr) => {
self.visit_expr(let_expr.init);
self.bind_pattern(let_expr.pat, let_expr.init);
}
ExprKind::Tup(expressions) | ExprKind::Array(expressions) => {
for expression in *expressions {
self.visit_expr(expression);
}
}
ExprKind::Struct(_, fields, tail) => {
for field in *fields {
self.visit_expr(field.expr);
}
if let rustc_hir::StructTailExpr::Base(base) = tail {
self.visit_expr(base);
}
}
ExprKind::Ret(value) => {
if let Some(value) = value {
self.visit_expr(value);
}
if !value.is_some_and(|value| self.returns_failure(value, false)) {
self.escapes.push(self.guards.len());
}
}
ExprKind::Break(_, Some(inner)) => self.visit_expr(inner),
ExprKind::Unary(_, inner)
| ExprKind::Use(inner, _)
| ExprKind::Cast(inner, _)
| ExprKind::Type(inner, _)
| ExprKind::DropTemps(inner)
| ExprKind::Field(inner, _)
| ExprKind::Repeat(inner, _)
| ExprKind::Yield(inner, _)
| ExprKind::Become(inner)
| ExprKind::UnsafeBinderCast(_, inner, _) => self.visit_expr(inner),
_ => {}
}
}
}
fn returned(value: GuardValue, short_circuits: Vec<HirId>) -> Enforcement {
enforced_if(value == GuardValue::Fallible, short_circuits)
}
fn enforced_if(enforced: bool, short_circuits: Vec<HirId>) -> Enforcement {
if enforced {
Enforcement::Enforced(short_circuits)
} else {
Enforcement::Unenforced
}
}
fn combine_branches(exits: impl IntoIterator<Item = Exit>) -> Exit {
let mut combined = Exit::Failure;
for exit in exits {
match exit {
Exit::Escape => return Exit::Escape,
Exit::FallThrough => combined = Exit::FallThrough,
Exit::Failure => {}
}
}
combined
}
fn classify_pattern(pattern: &Pat<'_>) -> PatternClass {
let variant = match &pattern.kind {
PatKind::TupleStruct(qpath, ..) => qpath_name(qpath),
PatKind::Expr(expr) => {
match &expr.kind {
PatExprKind::Path(qpath) => qpath_name(qpath),
_ => None,
}
}
PatKind::Binding(.., Some(inner)) => return classify_pattern(inner),
PatKind::Or(alternatives) => {
let mut classes = alternatives
.iter()
.map(|alternative| classify_pattern(alternative));
let first = classes.next().unwrap_or(PatternClass::Both);
return if classes.all(|class| class == first) {
first
} else {
PatternClass::Both
};
}
_ => None,
};
match variant {
Some("Ok" | "Some") => PatternClass::Success,
Some("Err" | "None") => PatternClass::Failure,
_ => PatternClass::Both,
}
}
fn is_try_branch(tcx: rustc_middle::ty::TyCtxt<'_>, call: &Expr<'_>) -> bool {
matches!(
tcx.parent_hir_node(call.hir_id),
Node::Expr(Expr {
kind: ExprKind::Match(scrutinee, _, MatchSource::TryDesugar(_)),
..
}) if scrutinee.hir_id == call.hir_id
)
}
fn bool_literal(expr: &Expr<'_>) -> Option<bool> {
match &expr.kind {
ExprKind::Lit(literal) => {
match literal.node {
LitKind::Bool(value) => Some(value),
_ => None,
}
}
ExprKind::DropTemps(inner) => bool_literal(inner),
_ => None,
}
}
fn bang_macro_name(span: Span) -> Option<rustc_span::Symbol> {
match span.ctxt().outer_expn_data().kind {
ExpnKind::Macro(MacroKind::Bang, name) => Some(name),
_ => None,
}
}
fn is_core_item(cx: &LateContext<'_>, definition: DefId, names: &[&str]) -> bool {
cx.tcx.crate_name(definition.krate).as_str() == "core"
&& names.contains(&cx.tcx.item_name(definition).as_str())
}
fn variant_constructed(
cx: &LateContext<'_>,
typeck: &TypeckResults<'_>,
expr: &Expr<'_>,
) -> Option<rustc_span::Symbol> {
let (qpath, path_id) = match &expr.kind {
ExprKind::Call(callee, _) => {
match &callee.kind {
ExprKind::Path(qpath) => (qpath, callee.hir_id),
_ => return None,
}
}
ExprKind::Path(qpath) => (qpath, expr.hir_id),
ExprKind::DropTemps(inner) | ExprKind::Use(inner, _) | ExprKind::Type(inner, _) => {
return variant_constructed(cx, typeck, inner);
}
_ => return None,
};
let Res::Def(DefKind::Ctor(CtorOf::Variant, _), constructor) = typeck.qpath_res(qpath, path_id)
else {
return None;
};
let variant = cx.tcx.parent(constructor);
is_core_item(cx, cx.tcx.parent(variant), &["Result", "Option"])
.then(|| cx.tcx.item_name(variant))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Outcome {
Succeeds,
Fails,
}
fn returns_constant(
cx: &LateContext<'_>,
local: LocalDefId,
body: &Body<'_>,
value: GuardValue,
outcome: Outcome,
) -> bool {
struct Bindings<'hir> {
initializers: HashMap<HirId, &'hir Expr<'hir>>,
reassigned: HashSet<HirId>,
}
impl<'hir> Visitor<'hir> for Bindings<'hir> {
fn visit_local(&mut self, local: &'hir LetStmt<'hir>) {
if let (PatKind::Binding(_, binding, _, None), Some(init)) =
(local.pat.kind, local.init)
{
self.initializers.insert(binding, init);
}
rustc_hir::intravisit::walk_local(self, local);
}
fn visit_expr(&mut self, expr: &'hir Expr<'hir>) {
let place = match &expr.kind {
ExprKind::Assign(place, ..) | ExprKind::AssignOp(_, place, _) => Some(*place),
ExprKind::AddrOf(_, mutability, place) if mutability.is_mut() => Some(*place),
_ => None,
};
if let Some(binding) = place.and_then(shared::expression_local_binding) {
self.reassigned.insert(binding);
}
rustc_hir::intravisit::walk_expr(self, expr);
}
}
struct Scan<'a, 'b, 'hir, 'tcx> {
cx: &'a LateContext<'tcx>,
typeck: &'b TypeckResults<'tcx>,
bindings: Bindings<'hir>,
value: GuardValue,
outcome: Outcome,
varies: bool,
}
impl<'hir> Scan<'_, '_, 'hir, '_> {
fn constant(&self, returned: &'hir Expr<'hir>, depth: usize) -> bool {
if let ExprKind::Path(QPath::Resolved(_, path)) = &returned.kind
&& let Res::Local(binding) = path.res
{
return depth < 8
&& !self.bindings.reassigned.contains(&binding)
&& self
.bindings
.initializers
.get(&binding)
.is_some_and(|init| self.constant(init, depth + 1));
}
match (self.outcome, self.value) {
(Outcome::Succeeds, GuardValue::Fallible) => {
variant_constructed(self.cx, self.typeck, returned)
.is_some_and(|variant| matches!(variant.as_str(), "Ok" | "Some"))
}
(Outcome::Succeeds, GuardValue::Flag(_)) => bool_literal(returned).is_some(),
(Outcome::Fails, _) => {
variant_constructed(self.cx, self.typeck, returned)
.is_some_and(|variant| matches!(variant.as_str(), "Err" | "None"))
|| self.typeck.expr_ty(returned).is_never()
}
}
}
}
impl<'hir> Visitor<'hir> for Scan<'_, '_, 'hir, '_> {
fn visit_expr(&mut self, expr: &'hir Expr<'hir>) {
if let ExprKind::If(condition, then, otherwise) = &expr.kind
&& let Some(taken) = bool_literal(condition)
{
self.visit_expr(condition);
if taken {
self.visit_expr(then);
} else if let Some(otherwise) = otherwise {
self.visit_expr(otherwise);
}
return;
}
let varies = match (&expr.kind, self.outcome) {
(ExprKind::Ret(Some(returned)), _) => !self.constant(returned, 0),
(ExprKind::Ret(None), _) => false,
(ExprKind::Match(_, _, MatchSource::TryDesugar(_)), Outcome::Succeeds) => true,
(ExprKind::Call(..) | ExprKind::MethodCall(..), Outcome::Succeeds) => {
self.typeck.expr_ty(expr).is_never()
}
(_, Outcome::Succeeds) => {
variant_constructed(self.cx, self.typeck, expr)
.is_some_and(|variant| matches!(variant.as_str(), "Err" | "None"))
}
(_, Outcome::Fails) => false,
};
self.varies |= varies;
rustc_hir::intravisit::walk_expr(self, expr);
}
}
let mut bindings = Bindings {
initializers: HashMap::new(),
reassigned: HashSet::new(),
};
bindings.visit_expr(body.value);
let mut tail = body.value;
while let ExprKind::Block(block, _) = &tail.kind
&& let Some(inner) = block.expr
{
tail = inner;
}
let mut scan = Scan {
cx,
typeck: cx.tcx.typeck(local),
bindings,
value,
outcome,
varies: false,
};
let tail_is_block_without_value =
matches!(&tail.kind, ExprKind::Block(block, _) if block.expr.is_none());
if !tail_is_block_without_value && !scan.constant(tail, 0) {
return false;
}
scan.visit_expr(body.value);
!scan.varies
}
fn body_can_panic(cx: &LateContext<'_>, local: LocalDefId, body: &Body<'_>) -> bool {
struct PanicFinder<'b, 'tcx> {
typeck: &'b TypeckResults<'tcx>,
panics: bool,
}
impl<'hir> Visitor<'hir> for PanicFinder<'_, '_> {
fn visit_expr(&mut self, expr: &'hir Expr<'hir>) {
if let ExprKind::If(condition, then, otherwise) = &expr.kind
&& let Some(taken) = bool_literal(condition)
{
if taken {
self.visit_expr(then);
} else if let Some(otherwise) = otherwise {
self.visit_expr(otherwise);
}
return;
}
if matches!(expr.kind, ExprKind::Call(..) | ExprKind::MethodCall(..))
&& self.typeck.expr_ty(expr).is_never()
{
self.panics = true;
}
rustc_hir::intravisit::walk_expr(self, expr);
}
}
let mut finder = PanicFinder {
typeck: cx.tcx.typeck(local),
panics: false,
};
finder.visit_expr(body.value);
finder.panics
}
fn peel_reference<'a, 'hir>(expr: &'a Expr<'hir>) -> &'a Expr<'hir> {
match &expr.kind {
ExprKind::AddrOf(_, _, inner) => inner,
_ => expr,
}
}
fn covers_class(pattern: &Pat<'_>) -> bool {
match &pattern.kind {
PatKind::TupleStruct(_, fields, _) => fields.iter().all(|field| irrefutable(field)),
PatKind::Expr(_) => classify_pattern(pattern) != PatternClass::Both,
_ => irrefutable(pattern),
}
}
fn irrefutable(pattern: &Pat<'_>) -> bool {
match &pattern.kind {
PatKind::Wild => true,
PatKind::Binding(.., sub) => sub.is_none_or(|sub| irrefutable(sub)),
PatKind::Tuple(fields, _) => fields.iter().all(|field| irrefutable(field)),
PatKind::Ref(inner, ..) | PatKind::Deref(inner) => irrefutable(inner),
_ => false,
}
}
fn breaks_to<'hir>(block: &'hir Expr<'hir>) -> std::vec::IntoIter<Option<&'hir Expr<'hir>>> {
struct BreakFinder<'hir> {
targets: [HirId; 2],
values: Vec<Option<&'hir Expr<'hir>>>,
}
impl<'hir> Visitor<'hir> for BreakFinder<'hir> {
fn visit_expr(&mut self, expr: &'hir Expr<'hir>) {
if let ExprKind::Break(destination, value) = &expr.kind
&& destination
.target_id
.is_ok_and(|target| self.targets.contains(&target))
{
self.values.push(*value);
}
rustc_hir::intravisit::walk_expr(self, expr);
}
}
let ExprKind::Block(inner, Some(_)) = &block.kind else {
return Vec::new().into_iter();
};
let mut finder = BreakFinder {
targets: [block.hir_id, inner.hir_id],
values: Vec::new(),
};
finder.visit_block(inner);
finder.values.into_iter()
}
fn names_guard(name: &str) -> bool {
let lowercase = name.to_ascii_lowercase();
GUARD_TERMS.iter().any(|term| lowercase.contains(term))
}
fn qpath_name<'hir>(qpath: &QPath<'hir>) -> Option<&'hir str> {
match qpath {
QPath::Resolved(_, path) => {
path.segments
.last()
.map(|segment| segment.ident.name.as_str())
}
QPath::TypeRelative(_, segment) => Some(segment.ident.name.as_str()),
}
}
fn locals_in<'hir>(expr: &'hir Expr<'hir>) -> Vec<HirId> {
struct LocalFinder {
locals: Vec<HirId>,
}
impl<'hir> Visitor<'hir> for LocalFinder {
fn visit_path(&mut self, path: &rustc_hir::Path<'hir>, _: HirId) {
if let Res::Local(binding) = path.res {
self.locals.push(binding);
}
rustc_hir::intravisit::walk_path(self, path);
}
}
let mut finder = LocalFinder { locals: Vec::new() };
finder.visit_expr(expr);
finder.locals
}
impl<'tcx> LateLintPass<'tcx> for RequireGuardedFullBalanceDrain {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
_: FnKind<'tcx>,
_: &'tcx rustc_hir::FnDecl<'tcx>,
body: &'tcx Body<'tcx>,
_: Span,
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 analyzer = DrainAnalyzer::analyze(cx, cx.typeck_results(), body, vec![def_id], None);
for span in &analyzer.order {
let Some(facts) = analyzer.drains.get(span) else {
continue;
};
if !facts.full_balance || facts.guarded || facts.closing {
continue;
}
diagnostics::emit(cx, REQUIRE_GUARDED_FULL_BALANCE_DRAIN, |diag| {
diag.span(*span);
diag.primary_message(
"an instruction path can sweep an account's entire balance in one call",
);
diag.help(
"gate full-balance sweeps behind a pause or circuit-breaker check (a pause \
flag plus a per-window withdrawal cap bounds a compromised key's blast \
radius)",
);
diag.help(
"a guard counts only when it reads a parameter-derived receiver or argument \
and its failure stops the handler first: `?`, `unwrap`/`expect`, or an \
`if`/`match`/`let ... else` whose failing branch returns `Err`/`None` or \
panics; a branch that returns `Ok` does not count",
);
diag.help(
"a differently named local wrapper counts when it returns `Result`/`Option` \
and enforces a named guard in its outermost scope before any early success \
`return` or `break`",
);
diag.help(
"if this drain is an account-close path, use `close_account_zeroed` so \
`require_zeroed_before_close` covers the stale-data risk too",
);
diag.help(shared::CONTROL_FLOW_LIMITATION_HELP);
});
}
}
}