//! A simple query to collect tall locals (upvars) a closure use. use hir_def::{ DefWithBodyId, expr_store::{Body, path::Path}, hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat}, resolver::{HasResolver, Resolver, ValueNs}, }; use hir_expand::mod_path::PathKind; use rustc_hash::{FxHashMap, FxHashSet}; use crate::db::HirDatabase; #[derive(Debug, Clone, PartialEq, Eq, Hash)] // Kept sorted. pub struct Upvars(Box<[BindingId]>); impl Upvars { fn new(upvars: &FxHashSet) -> Upvars { let mut upvars = upvars.iter().copied().collect::>(); upvars.sort_unstable(); Upvars(upvars) } #[inline] pub fn contains(&self, local: BindingId) -> bool { self.0.binary_search(&local).is_ok() } #[inline] pub fn iter(&self) -> impl ExactSizeIterator { self.0.iter().copied() } #[inline] pub fn is_empty(&self) -> bool { self.0.is_empty() } } /// Returns a map from `Expr::Closure` to its upvars. #[salsa::tracked(returns(as_deref))] pub fn upvars_mentioned( db: &dyn HirDatabase, owner: DefWithBodyId, ) -> Option>> { let body = db.body(owner); let mut resolver = owner.resolver(db); let mut result = FxHashMap::default(); handle_expr_outside_closure(db, &mut resolver, owner, &body, body.body_expr, &mut result); return if result.is_empty() { None } else { result.shrink_to_fit(); Some(Box::new(result)) }; fn handle_expr_outside_closure<'db>( db: &'db dyn HirDatabase, resolver: &mut Resolver<'db>, owner: DefWithBodyId, body: &Body, expr: ExprId, closures_map: &mut FxHashMap, ) { match &body[expr] { &Expr::Closure { body: body_expr, .. } => { let mut upvars = FxHashSet::default(); handle_expr_inside_closure( db, resolver, owner, body, expr, body_expr, &mut upvars, closures_map, ); if !upvars.is_empty() { closures_map.insert(expr, Upvars::new(&upvars)); } } _ => body.walk_child_exprs(expr, |expr| { handle_expr_outside_closure(db, resolver, owner, body, expr, closures_map) }), } } fn handle_expr_inside_closure<'db>( db: &'db dyn HirDatabase, resolver: &mut Resolver<'db>, owner: DefWithBodyId, body: &Body, current_closure: ExprId, expr: ExprId, upvars: &mut FxHashSet, closures_map: &mut FxHashMap, ) { match &body[expr] { Expr::Path(path) => { resolve_maybe_upvar( db, resolver, owner, body, current_closure, expr, expr.into(), upvars, path, ); } &Expr::Assignment { target, .. } => { body.walk_pats(target, &mut |pat| { let Pat::Path(path) = &body[pat] else { return }; resolve_maybe_upvar( db, resolver, owner, body, current_closure, expr, pat.into(), upvars, path, ); }); } &Expr::Closure { body: body_expr, .. } => { let mut closure_upvars = FxHashSet::default(); handle_expr_inside_closure( db, resolver, owner, body, expr, body_expr, &mut closure_upvars, closures_map, ); if !closure_upvars.is_empty() { closures_map.insert(expr, Upvars::new(&closure_upvars)); // All nested closure's upvars are also upvars of the parent closure. upvars.extend( closure_upvars .iter() .copied() .filter(|local| body.binding_owner(*local) != Some(current_closure)), ); } return; } _ => {} } body.walk_child_exprs(expr, |expr| { handle_expr_inside_closure( db, resolver, owner, body, current_closure, expr, upvars, closures_map, ) }); } } fn resolve_maybe_upvar<'db>( db: &'db dyn HirDatabase, resolver: &mut Resolver<'db>, owner: DefWithBodyId, body: &Body, current_closure: ExprId, expr: ExprId, id: ExprOrPatId, upvars: &mut FxHashSet, path: &Path, ) { if let Path::BarePath(mod_path) = path && matches!(mod_path.kind, PathKind::Plain) && mod_path.segments().len() == 1 { // Could be a variable. let guard = resolver.update_to_inner_scope(db, owner, expr); let resolution = resolver.resolve_path_in_value_ns_fully(db, path, body.expr_or_pat_path_hygiene(id)); if let Some(ValueNs::LocalBinding(local)) = resolution && body.binding_owner(local) != Some(current_closure) { upvars.insert(local); } resolver.reset_to_guard(guard); } } #[cfg(test)] mod tests { use expect_test::{Expect, expect}; use hir_def::{ModuleDefId, db::DefDatabase, nameres::crate_def_map}; use itertools::Itertools; use span::Edition; use test_fixture::WithFixture; use crate::{test_db::TestDB, upvars::upvars_mentioned}; #[track_caller] fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, expectation: Expect) { let db = TestDB::with_files(ra_fixture); crate::attach_db(&db, || { let def_map = crate_def_map(&db, db.test_crate()); let func = def_map .modules() .flat_map(|(_, module)| module.scope.declarations()) .filter_map(|decl| match decl { ModuleDefId::FunctionId(func) => Some(func), _ => None, }) .exactly_one() .unwrap_or_else(|_| panic!("expected one function")); let (body, source_map) = db.body_with_source_map(func.into()); let Some(upvars) = upvars_mentioned(&db, func.into()) else { expectation.assert_eq(""); return; }; let mut closures = Vec::new(); for (&closure, upvars) in upvars { let closure_range = source_map.expr_syntax(closure).unwrap().value.text_range(); let upvars = upvars .iter() .map(|local| body[local].name.display(&db, Edition::CURRENT)) .join(", "); closures.push((closure_range, upvars)); } closures.sort_unstable_by_key(|(range, _)| (range.start(), range.end())); let closures = closures .into_iter() .map(|(range, upvars)| format!("{range:?}: {upvars}")) .join("\n"); expectation.assert_eq(&closures); }); } #[test] fn simple() { check( r#" struct foo; fn foo(param: i32) { let local = "boo"; || { param; foo }; || local; || { param; local; param; local; }; || 0xDEAFBEAF; } "#, expect![[r#" 60..77: param 83..91: local 97..131: param, local"#]], ); } #[test] fn nested() { check( r#" fn foo() { let (a, b); || { || a; || b; }; } "#, expect![[r#" 31..69: a, b 44..48: a 58..62: b"#]], ); } #[test] fn closure_var() { check( r#" fn foo() { let upvar = 1; |closure_param: i32| { let closure_local = closure_param; closure_local + upvar }; } "#, expect!["34..135: upvar"], ); } #[test] fn closure_var_nested() { check( r#" fn foo() { let a = 1; |b: i32| { || { let c = 123; a + b + c } }; } "#, expect![[r#" 30..116: a 49..110: a, b"#]], ); } }