Skip to main content

hir_ty/
upvars.rs

1//! A simple query to collect tall locals (upvars) a closure use.
2
3use hir_def::{
4    DefWithBodyId, ExpressionStoreOwnerId, GenericDefId, VariantId,
5    expr_store::{ExpressionStore, path::Path},
6    hir::{BindingId, Expr, ExprId, ExprOrPatId},
7    resolver::{HasResolver, Resolver, ValueNs},
8};
9use hir_expand::mod_path::PathKind;
10use rustc_hash::{FxHashMap, FxHashSet};
11
12use crate::db::HirDatabase;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15// Kept sorted.
16pub struct Upvars(Box<[BindingId]>);
17
18impl Upvars {
19    fn new(upvars: &FxHashSet<BindingId>) -> Upvars {
20        let mut upvars = upvars.iter().copied().collect::<Box<[_]>>();
21        upvars.sort_unstable();
22        Upvars(upvars)
23    }
24
25    #[inline]
26    pub fn contains(&self, local: BindingId) -> bool {
27        self.0.binary_search(&local).is_ok()
28    }
29
30    #[inline]
31    pub fn iter(&self) -> impl ExactSizeIterator<Item = BindingId> {
32        self.0.iter().copied()
33    }
34
35    #[inline]
36    pub fn is_empty(&self) -> bool {
37        self.0.is_empty()
38    }
39
40    #[inline]
41    pub fn as_ref(&self) -> UpvarsRef<'_> {
42        UpvarsRef(&self.0)
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47// Kept sorted.
48pub struct UpvarsRef<'db>(&'db [BindingId]);
49
50impl UpvarsRef<'_> {
51    #[inline]
52    pub fn contains(self, local: BindingId) -> bool {
53        self.0.binary_search(&local).is_ok()
54    }
55
56    #[inline]
57    pub fn iter(self) -> impl ExactSizeIterator<Item = BindingId> {
58        self.0.iter().copied()
59    }
60
61    #[inline]
62    pub fn is_empty(self) -> bool {
63        self.0.is_empty()
64    }
65
66    #[inline]
67    pub const fn empty() -> Self {
68        UpvarsRef(&[])
69    }
70}
71
72/// Returns a map from `Expr::Closure` to its upvars.
73pub fn upvars_mentioned(
74    db: &dyn HirDatabase,
75    owner: ExpressionStoreOwnerId,
76) -> Option<&FxHashMap<ExprId, Upvars>> {
77    return match owner {
78        ExpressionStoreOwnerId::Signature(owner) => signature_upvars_mentioned(db, owner),
79        ExpressionStoreOwnerId::Body(owner) => body_upvars_mentioned(db, owner),
80        ExpressionStoreOwnerId::VariantFields(owner) => variant_fields_upvars_mentioned(db, owner),
81    };
82
83    #[salsa::tracked(returns(as_deref))]
84    pub fn signature_upvars_mentioned(
85        db: &dyn HirDatabase,
86        owner: GenericDefId,
87    ) -> Option<Box<FxHashMap<ExprId, Upvars>>> {
88        upvars_mentioned_impl(db, owner.into())
89    }
90
91    #[salsa::tracked(returns(as_deref))]
92    pub fn body_upvars_mentioned(
93        db: &dyn HirDatabase,
94        owner: DefWithBodyId,
95    ) -> Option<Box<FxHashMap<ExprId, Upvars>>> {
96        upvars_mentioned_impl(db, owner.into())
97    }
98
99    #[salsa::tracked(returns(as_deref))]
100    pub fn variant_fields_upvars_mentioned(
101        db: &dyn HirDatabase,
102        owner: VariantId,
103    ) -> Option<Box<FxHashMap<ExprId, Upvars>>> {
104        upvars_mentioned_impl(db, owner.into())
105    }
106}
107
108pub fn upvars_mentioned_impl(
109    db: &dyn HirDatabase,
110    owner: ExpressionStoreOwnerId,
111) -> Option<Box<FxHashMap<ExprId, Upvars>>> {
112    let store = ExpressionStore::of(db, owner);
113    store.expr_roots().next()?;
114    let mut resolver = owner.resolver(db);
115    let mut result = FxHashMap::default();
116    for root_expr in store.expr_roots() {
117        handle_expr_outside_closure(db, &mut resolver, owner, store, root_expr, &mut result);
118    }
119    return if result.is_empty() {
120        None
121    } else {
122        result.shrink_to_fit();
123        Some(Box::new(result))
124    };
125
126    fn handle_expr_outside_closure<'db>(
127        db: &'db dyn HirDatabase,
128        resolver: &mut Resolver<'db>,
129        owner: ExpressionStoreOwnerId,
130        body: &ExpressionStore,
131        expr: ExprId,
132        closures_map: &mut FxHashMap<ExprId, Upvars>,
133    ) {
134        match &body[expr] {
135            &Expr::Closure { body: body_expr, .. } => {
136                let mut upvars = FxHashSet::default();
137                handle_expr_inside_closure(
138                    db,
139                    resolver,
140                    owner,
141                    body,
142                    expr,
143                    body_expr,
144                    &mut upvars,
145                    closures_map,
146                );
147                if !upvars.is_empty() {
148                    closures_map.insert(expr, Upvars::new(&upvars));
149                }
150            }
151            _ => body.walk_child_exprs(expr, |expr| {
152                handle_expr_outside_closure(db, resolver, owner, body, expr, closures_map)
153            }),
154        }
155    }
156
157    fn handle_expr_inside_closure<'db>(
158        db: &'db dyn HirDatabase,
159        resolver: &mut Resolver<'db>,
160        owner: ExpressionStoreOwnerId,
161        body: &ExpressionStore,
162        current_closure: ExprId,
163        expr: ExprId,
164        upvars: &mut FxHashSet<BindingId>,
165        closures_map: &mut FxHashMap<ExprId, Upvars>,
166    ) {
167        match &body[expr] {
168            Expr::Path(path) => {
169                resolve_maybe_upvar(
170                    db,
171                    resolver,
172                    owner,
173                    body,
174                    current_closure,
175                    expr,
176                    expr.into(),
177                    upvars,
178                    path,
179                );
180            }
181            &Expr::Closure { body: body_expr, .. } => {
182                let mut closure_upvars = FxHashSet::default();
183                handle_expr_inside_closure(
184                    db,
185                    resolver,
186                    owner,
187                    body,
188                    expr,
189                    body_expr,
190                    &mut closure_upvars,
191                    closures_map,
192                );
193                if !closure_upvars.is_empty() {
194                    closures_map.insert(expr, Upvars::new(&closure_upvars));
195                    // All nested closure's upvars are also upvars of the parent closure.
196                    upvars.extend(
197                        closure_upvars
198                            .iter()
199                            .copied()
200                            .filter(|local| body.binding_owner(*local) != Some(current_closure)),
201                    );
202                }
203                return;
204            }
205            _ => {}
206        }
207        body.walk_child_exprs(expr, |expr| {
208            handle_expr_inside_closure(
209                db,
210                resolver,
211                owner,
212                body,
213                current_closure,
214                expr,
215                upvars,
216                closures_map,
217            )
218        });
219    }
220}
221
222fn resolve_maybe_upvar<'db>(
223    db: &'db dyn HirDatabase,
224    resolver: &mut Resolver<'db>,
225    owner: ExpressionStoreOwnerId,
226    body: &ExpressionStore,
227    current_closure: ExprId,
228    expr: ExprId,
229    id: ExprOrPatId,
230    upvars: &mut FxHashSet<BindingId>,
231    path: &Path,
232) {
233    if let Path::BarePath(mod_path) = path
234        && matches!(mod_path.kind, PathKind::Plain | PathKind::SELF)
235        // `self` is length zero.
236        && mod_path.segments().len() <= 1
237    {
238        // Could be a variable.
239        let guard = resolver.update_to_inner_scope(db, owner, expr);
240        let resolution =
241            resolver.resolve_path_in_value_ns_fully(db, path, body.expr_or_pat_path_hygiene(id));
242        if let Some(ValueNs::LocalBinding(local)) = resolution
243            && body.binding_owner(local) != Some(current_closure)
244        {
245            upvars.insert(local);
246        }
247        resolver.reset_to_guard(guard);
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use expect_test::{Expect, expect};
254    use hir_def::{
255        AssocItemId, DefWithBodyId, ModuleDefId, expr_store::Body, nameres::crate_def_map,
256    };
257    use itertools::Itertools;
258    use span::Edition;
259    use test_fixture::WithFixture;
260
261    use crate::{test_db::TestDB, upvars::upvars_mentioned};
262
263    #[track_caller]
264    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, expectation: Expect) {
265        let db = TestDB::with_files(ra_fixture);
266        crate::attach_db(&db, || {
267            let def_map = crate_def_map(&db, db.test_crate());
268            let func = def_map
269                .modules()
270                .flat_map(|(_, module)| module.scope.declarations())
271                .filter_map(|decl| match decl {
272                    ModuleDefId::FunctionId(func) => Some(func),
273                    _ => None,
274                })
275                .chain(def_map.modules().flat_map(|(_, module)| {
276                    module.scope.impls().flat_map(|impl_| &*impl_.impl_items(&db).items).filter_map(
277                        |&(_, item)| match item {
278                            AssocItemId::FunctionId(it) => Some(it),
279                            _ => None,
280                        },
281                    )
282                }))
283                .exactly_one()
284                .unwrap_or_else(|_| panic!("expected one function"));
285            let (body, source_map) = Body::with_source_map(&db, func.into());
286            let Some(upvars) = upvars_mentioned(&db, DefWithBodyId::from(func).into()) else {
287                expectation.assert_eq("");
288                return;
289            };
290            let mut closures = Vec::new();
291            for (&closure, upvars) in upvars {
292                let closure_range = source_map.expr_syntax(closure).unwrap().value.text_range();
293                let upvars = upvars
294                    .iter()
295                    .map(|local| body[local].name.display(&db, Edition::CURRENT))
296                    .join(", ");
297                closures.push((closure_range, upvars));
298            }
299            closures.sort_unstable_by_key(|(range, _)| (range.start(), range.end()));
300            let closures = closures
301                .into_iter()
302                .map(|(range, upvars)| format!("{range:?}: {upvars}"))
303                .join("\n");
304            expectation.assert_eq(&closures);
305        });
306    }
307
308    #[test]
309    fn simple() {
310        check(
311            r#"
312struct foo;
313fn foo(param: i32) {
314    let local = "boo";
315    || { param; foo };
316    || local;
317    || { param; local; param; local; };
318    || 0xDEAFBEAF;
319}
320        "#,
321            expect![[r#"
322                60..77: param
323                83..91: local
324                97..131: param, local"#]],
325        );
326    }
327
328    #[test]
329    fn nested() {
330        check(
331            r#"
332fn foo() {
333    let (a, b);
334    || {
335        || a;
336        || b;
337    };
338}
339        "#,
340            expect![[r#"
341                31..69: a, b
342                44..48: a
343                58..62: b"#]],
344        );
345    }
346
347    #[test]
348    fn closure_var() {
349        check(
350            r#"
351fn foo() {
352    let upvar = 1;
353    |closure_param: i32| {
354        let closure_local = closure_param;
355        closure_local + upvar
356    };
357}
358        "#,
359            expect!["34..135: upvar"],
360        );
361    }
362
363    #[test]
364    fn closure_var_nested() {
365        check(
366            r#"
367fn foo() {
368    let a = 1;
369    |b: i32| {
370        || {
371            let c = 123;
372            a + b + c
373        }
374    };
375}
376        "#,
377            expect![[r#"
378                30..116: a
379                49..110: a, b"#]],
380        );
381    }
382
383    #[test]
384    fn self_upvar() {
385        check(
386            r#"
387struct Foo(i32);
388impl Foo {
389    fn foo(&self) {
390        || self.0;
391    }
392}
393        "#,
394            expect!["56..65: self"],
395        );
396    }
397}