extern crate rustc_errors;
extern crate rustc_hir;
extern crate rustc_span;
use std::collections::HashMap;
use std::collections::HashSet;
use rustc_hir::Expr;
use rustc_hir::ExprKind;
use rustc_hir::Pat;
use rustc_hir::StmtKind;
use rustc_hir::intravisit::Visitor;
use rustc_lint::LateContext;
use rustc_lint::LateLintPass;
use rustc_lint::LintContext;
use rustc_span::Span;
crate::declare_late_lint! {
pub DENY_UNUSED_ACCOUNT_BORROW_GUARDS,
Warn,
"account borrow guards bound to locals must be read or discarded immediately"
}
fn is_account_borrow_guard(cx: &LateContext<'_>, pattern: &Pat<'_>) -> bool {
let ty = cx.typeck_results().pat_ty(pattern).peel_refs();
let Some(definition) = ty.ty_adt_def() else {
return false;
};
let definition = definition.did();
cx.tcx.crate_name(definition.krate).as_str() == "solana_account_view"
&& matches!(cx.tcx.item_name(definition).as_str(), "Ref" | "RefMut")
}
fn is_drop_callee(cx: &LateContext<'_>, callee: &Expr<'_>) -> bool {
let ExprKind::Path(rustc_hir::QPath::Resolved(_, path)) = &callee.kind else {
return false;
};
match path.res {
rustc_hir::def::Res::Def(_, def_id) => {
matches!(
cx.tcx.def_path_str(def_id).as_str(),
"std::mem::drop" | "core::mem::drop"
)
}
_ => false,
}
}
#[coverage(off)]
fn local_binding(expr: &Expr<'_>) -> Option<rustc_hir::hir_id::HirId> {
match &expr.kind {
ExprKind::Path(rustc_hir::QPath::Resolved(_, path)) => {
match path.res {
rustc_hir::def::Res::Local(binding) => Some(binding),
_ => None,
}
}
_ => None,
}
}
struct GuardBinding {
hir_id: rustc_hir::hir_id::HirId,
name: String,
span: Span,
statement_span: Span,
initializer_span: Option<Span>,
}
struct GuardPatternCollector<'cx, 'tcx, 'guards> {
cx: &'cx LateContext<'tcx>,
guards: &'guards mut Vec<GuardBinding>,
root_pattern: rustc_hir::hir_id::HirId,
statement_span: Span,
initializer_span: Span,
}
impl<'tcx> Visitor<'tcx> for GuardPatternCollector<'_, 'tcx, '_> {
fn visit_pat(&mut self, pattern: &'tcx Pat<'tcx>) {
if let rustc_hir::PatKind::Binding(_, binding, ident, subpattern) = pattern.kind
&& is_account_borrow_guard(self.cx, pattern)
{
let is_plain_root_binding = pattern.hir_id == self.root_pattern && subpattern.is_none();
self.guards.push(GuardBinding {
hir_id: binding,
name: ident.name.to_string(),
span: if is_plain_root_binding {
self.initializer_span
} else {
pattern.span
},
statement_span: self.statement_span,
initializer_span: is_plain_root_binding.then_some(self.initializer_span),
});
}
rustc_hir::intravisit::walk_pat(self, pattern);
}
}
struct Analyzer<'cx, 'tcx> {
cx: &'cx LateContext<'tcx>,
guards: Vec<GuardBinding>,
referenced: HashSet<rustc_hir::hir_id::HirId>,
all_drops: HashMap<rustc_hir::hir_id::HirId, usize>,
statement_drops: HashMap<rustc_hir::hir_id::HirId, Vec<Span>>,
}
impl<'cx, 'tcx> Analyzer<'cx, 'tcx> {
fn visit_arg_of_drop(&mut self, argument: &'tcx Expr<'tcx>) {
if let Some(binding) = local_binding(argument) {
self.all_drops
.entry(binding)
.and_modify(|count| *count += 1)
.or_insert(1);
} else {
self.visit_expr(argument);
}
}
}
impl<'tcx> Visitor<'tcx> for Analyzer<'_, 'tcx> {
fn visit_stmt(&mut self, statement: &'tcx rustc_hir::Stmt<'tcx>) {
if let StmtKind::Let(local) = statement.kind
&& let Some(initializer) = local.init
{
let mut collector = GuardPatternCollector {
cx: self.cx,
guards: &mut self.guards,
root_pattern: local.pat.hir_id,
statement_span: local.span,
initializer_span: initializer.span,
};
collector.visit_pat(local.pat);
if matches!(local.pat.kind, rustc_hir::PatKind::Wild)
&& local_binding(initializer).is_some()
{
return;
}
}
if let StmtKind::Semi(expr) = statement.kind
&& let ExprKind::Call(callee, args) = &expr.kind
&& is_drop_callee(self.cx, callee)
&& let Some(binding) = args.first().and_then(local_binding)
{
self.statement_drops
.entry(binding)
.or_default()
.push(statement.span);
}
rustc_hir::intravisit::walk_stmt(self, statement);
}
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
if let ExprKind::Call(callee, args) = &expr.kind
&& is_drop_callee(self.cx, callee)
{
self.visit_expr(callee);
for argument in *args {
self.visit_arg_of_drop(argument);
}
return;
}
if let ExprKind::Path(rustc_hir::QPath::Resolved(_, path)) = &expr.kind
&& let rustc_hir::def::Res::Local(binding) = path.res
{
self.referenced.insert(binding);
}
if let ExprKind::Closure(closure) = &expr.kind {
self.visit_body(self.cx.tcx.hir_body(closure.body));
return;
}
rustc_hir::intravisit::walk_expr(self, expr);
}
}
impl<'tcx> LateLintPass<'tcx> for DenyUnusedAccountBorrowGuards {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
_: rustc_hir::intravisit::FnKind<'tcx>,
_: &'tcx rustc_hir::FnDecl<'tcx>,
body: &'tcx rustc_hir::Body<'tcx>,
_: rustc_span::Span,
_: rustc_hir::def_id::LocalDefId,
) {
let mut analyzer = Analyzer {
cx,
guards: Vec::new(),
referenced: HashSet::new(),
all_drops: HashMap::new(),
statement_drops: HashMap::new(),
};
analyzer.visit_body(body);
let source_map = cx.sess().source_map();
for guard in analyzer.guards {
if analyzer.referenced.contains(&guard.hir_id) {
continue;
}
cx.lint(DENY_UNUSED_ACCOUNT_BORROW_GUARDS, |diag| {
diag.span(guard.span);
diag.primary_message(format!(
"account borrow guard `{}` is never read",
guard.name
));
diag.help(
"the guard holds the account data borrow until the end of its scope; discard \
the validation value immediately by calling it as a `?` statement, binding \
it with `let _ = ...`, or reading it before the scope ends",
);
let fix = suggestion::for_binding(
source_map,
&guard,
analyzer.all_drops.get(&guard.hir_id).copied().unwrap_or(0),
analyzer.statement_drops.get(&guard.hir_id),
);
if let Some(suggestion::Fix {
replacement,
applicability,
}) = fix
{
match replacement {
suggestion::Replacement::Single(span, snippet) => {
diag.span_suggestion(
span,
"discard this guard immediately",
snippet,
applicability,
);
}
suggestion::Replacement::Many(parts) => {
diag.multipart_suggestion(
"discard this guard immediately",
parts,
applicability,
);
}
}
}
});
}
}
}
mod suggestion {
extern crate rustc_errors;
extern crate rustc_span;
use rustc_span::Span;
use super::GuardBinding;
pub enum Replacement {
Single(Span, String),
Many(Vec<(Span, String)>),
}
pub struct Fix {
pub replacement: Replacement,
pub applicability: rustc_errors::Applicability,
}
pub fn for_binding(
source_map: &rustc_span::source_map::SourceMap,
guard: &GuardBinding,
all_drops: usize,
statement_drops: Option<&Vec<Span>>,
) -> Option<Fix> {
if guard.statement_span.from_expansion() {
return None;
}
let initializer_span = guard.initializer_span?;
let initializer = source_map.span_to_snippet(initializer_span).ok()? + ";";
if all_drops == 0 {
return Some(Fix {
replacement: Replacement::Single(guard.statement_span, initializer),
applicability: rustc_errors::Applicability::MachineApplicable,
});
}
let drop_spans = statement_drops.map_or(0, Vec::len);
if all_drops > drop_spans {
return None;
}
let statement_drops = statement_drops
.filter(|spans| !spans.is_empty())
.expect("all drops are statements");
let mut replacements = vec![(guard.statement_span, initializer)];
replacements.extend(statement_drops.iter().map(|span| (*span, String::new())));
Some(Fix {
replacement: Replacement::Many(replacements),
applicability: rustc_errors::Applicability::MaybeIncorrect,
})
}
}