swc_ecma_usage_analyzer/alias/
mod.rs

1#![allow(clippy::needless_update)]
2
3use rustc_hash::FxHashSet;
4use swc_common::SyntaxContext;
5use swc_ecma_ast::*;
6use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
7
8use self::ctx::Ctx;
9use crate::{marks::Marks, util::is_global_var_with_pure_property_access};
10
11mod ctx;
12
13#[derive(Default)]
14#[non_exhaustive]
15pub struct AliasConfig {
16    pub marks: Option<Marks>,
17    pub ignore_nested: bool,
18    /// TODO(kdy1): This field is used for sequential inliner.
19    /// It should be renamed to some correct name.
20    pub need_all: bool,
21
22    /// We can skip visiting children nodes in some cases.
23    ///
24    /// Because we recurse in the usage analyzer, we don't need to recurse into
25    /// child node that the usage analyzer will invoke [`collect_infects_from`]
26    /// on.
27    pub ignore_named_child_scope: bool,
28}
29
30impl AliasConfig {
31    pub fn marks(mut self, arg: Option<Marks>) -> Self {
32        self.marks = arg;
33        self
34    }
35
36    pub fn ignore_nested(mut self, arg: bool) -> Self {
37        self.ignore_nested = arg;
38        self
39    }
40
41    pub fn ignore_named_child_scope(mut self, arg: bool) -> Self {
42        self.ignore_named_child_scope = arg;
43        self
44    }
45
46    pub fn need_all(mut self, arg: bool) -> Self {
47        self.need_all = arg;
48        self
49    }
50}
51
52pub trait InfectableNode {
53    fn is_fn_or_arrow_expr(&self) -> bool;
54}
55
56impl InfectableNode for Function {
57    fn is_fn_or_arrow_expr(&self) -> bool {
58        false
59    }
60}
61
62impl InfectableNode for Expr {
63    fn is_fn_or_arrow_expr(&self) -> bool {
64        matches!(self, Expr::Arrow(..) | Expr::Fn(..))
65    }
66}
67
68impl<T> InfectableNode for Box<T>
69where
70    T: InfectableNode,
71{
72    fn is_fn_or_arrow_expr(&self) -> bool {
73        (**self).is_fn_or_arrow_expr()
74    }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78
79pub enum AccessKind {
80    Reference,
81    Call,
82}
83
84pub type Access = (Id, AccessKind);
85
86pub fn collect_infects_from<N>(node: &N, config: AliasConfig) -> FxHashSet<Access>
87where
88    N: InfectableNode + VisitWith<InfectionCollector>,
89{
90    if config.ignore_nested && node.is_fn_or_arrow_expr() {
91        return Default::default();
92    }
93
94    let unresolved_ctxt = config
95        .marks
96        .map(|m| SyntaxContext::empty().apply_mark(m.unresolved_mark));
97
98    let mut visitor = InfectionCollector {
99        config,
100        unresolved_ctxt,
101
102        ctx: Ctx::TrackExprIdent,
103
104        bindings: FxHashSet::default(),
105        accesses: FxHashSet::default(),
106
107        max_entries: None,
108    };
109
110    node.visit_with(&mut visitor);
111
112    visitor.accesses
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
116pub struct TooManyAccesses;
117
118/// If the number of accesses exceeds `max_entries`, it returns `Err(())`.
119pub fn try_collect_infects_from<N>(
120    node: &N,
121    config: AliasConfig,
122    max_entries: usize,
123) -> Result<FxHashSet<Access>, TooManyAccesses>
124where
125    N: InfectableNode + VisitWith<InfectionCollector>,
126{
127    if config.ignore_nested && node.is_fn_or_arrow_expr() {
128        return Ok(Default::default());
129    }
130
131    let unresolved_ctxt = config
132        .marks
133        .map(|m| SyntaxContext::empty().apply_mark(m.unresolved_mark));
134
135    let mut visitor = InfectionCollector {
136        config,
137        unresolved_ctxt,
138
139        ctx: Ctx::TrackExprIdent,
140
141        bindings: FxHashSet::default(),
142        accesses: FxHashSet::default(),
143
144        max_entries: Some(max_entries),
145    };
146
147    node.visit_with(&mut visitor);
148
149    if visitor.accesses.len() > max_entries {
150        return Err(TooManyAccesses);
151    }
152
153    Ok(visitor.accesses)
154}
155
156pub struct InfectionCollector {
157    config: AliasConfig,
158    unresolved_ctxt: Option<SyntaxContext>,
159
160    bindings: FxHashSet<Id>,
161
162    ctx: Ctx,
163
164    accesses: FxHashSet<Access>,
165
166    max_entries: Option<usize>,
167}
168
169impl InfectionCollector {
170    fn add_binding(&mut self, e: &Ident) {
171        if self.bindings.insert(e.to_id()) {
172            self.accesses.remove(&(e.to_id(), AccessKind::Reference));
173            self.accesses.remove(&(e.to_id(), AccessKind::Call));
174        }
175    }
176
177    fn add_usage(&mut self, e: Id) {
178        if self.bindings.contains(&e) {
179            return;
180        }
181
182        if self.unresolved_ctxt == Some(e.1) && is_global_var_with_pure_property_access(&e.0) {
183            return;
184        }
185
186        self.accesses.insert((
187            e,
188            if self.ctx.contains(Ctx::IsCallee) {
189                AccessKind::Call
190            } else {
191                AccessKind::Reference
192            },
193        ));
194    }
195}
196
197impl Visit for InfectionCollector {
198    noop_visit_type!();
199
200    fn visit_arrow_expr(&mut self, n: &ArrowExpr) {
201        let old = self.ctx.contains(Ctx::IsPatDecl);
202
203        for p in &n.params {
204            self.ctx.insert(Ctx::IsPatDecl);
205            p.visit_with(self);
206        }
207
208        n.body.visit_with(self);
209        self.ctx.set(Ctx::IsPatDecl, old);
210    }
211
212    fn visit_assign_expr(&mut self, n: &AssignExpr) {
213        if self.config.ignore_named_child_scope
214            && n.op == op!("=")
215            && n.left.as_simple().and_then(|l| l.leftmost()).is_some()
216        {
217            n.left.visit_with(self);
218            return;
219        }
220
221        n.visit_children_with(self);
222    }
223
224    fn visit_assign_pat_prop(&mut self, node: &AssignPatProp) {
225        node.value.visit_with(self);
226
227        if self.ctx.contains(Ctx::IsPatDecl) {
228            self.add_binding(&node.key.clone().into());
229        }
230    }
231
232    fn visit_bin_expr(&mut self, e: &BinExpr) {
233        match e.op {
234            op!("in")
235            | op!("instanceof")
236            | op!(bin, "-")
237            | op!(bin, "+")
238            | op!("/")
239            | op!("*")
240            | op!("%")
241            | op!("&")
242            | op!("^")
243            | op!("|")
244            | op!("==")
245            | op!("===")
246            | op!("!=")
247            | op!("!==")
248            | op!("<")
249            | op!("<=")
250            | op!(">")
251            | op!(">=")
252            | op!("<<")
253            | op!(">>")
254            | op!(">>>") => {
255                let ctx = self.ctx - Ctx::TrackExprIdent - Ctx::IsCallee;
256                e.visit_children_with(&mut *self.with_ctx(ctx));
257            }
258            _ => {
259                let ctx = (self.ctx | Ctx::TrackExprIdent) - Ctx::IsCallee;
260                e.visit_children_with(&mut *self.with_ctx(ctx));
261            }
262        }
263    }
264
265    fn visit_callee(&mut self, n: &Callee) {
266        let ctx = self.ctx | Ctx::IsCallee;
267        n.visit_children_with(&mut *self.with_ctx(ctx));
268    }
269
270    fn visit_class_decl(&mut self, node: &ClassDecl) {
271        self.add_binding(&node.ident);
272
273        node.visit_children_with(self);
274    }
275
276    fn visit_cond_expr(&mut self, e: &CondExpr) {
277        {
278            let ctx = self.ctx - Ctx::TrackExprIdent - Ctx::IsCallee;
279            e.test.visit_with(&mut *self.with_ctx(ctx));
280        }
281
282        {
283            let ctx = self.ctx | Ctx::TrackExprIdent;
284            e.cons.visit_with(&mut *self.with_ctx(ctx));
285            e.alt.visit_with(&mut *self.with_ctx(ctx));
286        }
287    }
288
289    fn visit_expr(&mut self, e: &Expr) {
290        if let Some(max_entries) = self.max_entries {
291            if self.accesses.len() >= max_entries {
292                return;
293            }
294        }
295
296        match e {
297            Expr::Ident(i) => {
298                if self.ctx.contains(Ctx::TrackExprIdent) {
299                    self.add_usage(i.to_id());
300                }
301            }
302
303            _ => {
304                let ctx = (self.ctx | Ctx::TrackExprIdent) - Ctx::IsPatDecl;
305                e.visit_children_with(&mut *self.with_ctx(ctx));
306            }
307        }
308    }
309
310    fn visit_fn_decl(&mut self, n: &FnDecl) {
311        self.add_binding(&n.ident);
312
313        if self.config.ignore_named_child_scope {
314            return;
315        }
316
317        n.visit_children_with(self);
318    }
319
320    fn visit_fn_expr(&mut self, n: &FnExpr) {
321        if self.config.ignore_named_child_scope && n.ident.is_some() {
322            return;
323        }
324        n.visit_children_with(self);
325    }
326
327    fn visit_function(&mut self, n: &Function) {
328        if let Some(max_entries) = self.max_entries {
329            if self.accesses.len() >= max_entries {
330                return;
331            }
332        }
333
334        n.visit_children_with(self);
335    }
336
337    fn visit_ident(&mut self, n: &Ident) {
338        self.add_usage(n.to_id());
339    }
340
341    fn visit_member_expr(&mut self, n: &MemberExpr) {
342        {
343            let mut ctx = self.ctx;
344            ctx.set(Ctx::TrackExprIdent, self.config.need_all);
345            n.obj.visit_with(&mut *self.with_ctx(ctx));
346        }
347
348        {
349            let mut ctx = self.ctx;
350            ctx.set(Ctx::TrackExprIdent, self.config.need_all);
351            n.prop.visit_with(&mut *self.with_ctx(ctx));
352        }
353    }
354
355    fn visit_member_prop(&mut self, n: &MemberProp) {
356        if let MemberProp::Computed(c) = &n {
357            c.visit_with(&mut *self.with_ctx(self.ctx - Ctx::IsCallee));
358        }
359    }
360
361    fn visit_param(&mut self, node: &Param) {
362        let old = self.ctx.contains(Ctx::IsPatDecl);
363        self.ctx.insert(Ctx::IsPatDecl);
364        node.visit_children_with(self);
365        self.ctx.set(Ctx::IsPatDecl, old);
366    }
367
368    fn visit_pat(&mut self, node: &Pat) {
369        node.visit_children_with(self);
370
371        if self.ctx.contains(Ctx::IsPatDecl) {
372            if let Pat::Ident(i) = node {
373                self.add_binding(i)
374            }
375        }
376    }
377
378    fn visit_prop_name(&mut self, n: &PropName) {
379        if let PropName::Computed(c) = &n {
380            c.visit_with(&mut *self.with_ctx(self.ctx - Ctx::IsCallee));
381        }
382    }
383
384    fn visit_stmt(&mut self, n: &Stmt) {
385        if let Some(max_entries) = self.max_entries {
386            if self.accesses.len() >= max_entries {
387                return;
388            }
389        }
390
391        n.visit_children_with(self);
392    }
393
394    fn visit_super_prop_expr(&mut self, n: &SuperPropExpr) {
395        if let SuperProp::Computed(c) = &n.prop {
396            c.visit_with(&mut *self.with_ctx(self.ctx - Ctx::IsCallee));
397        }
398    }
399
400    fn visit_unary_expr(&mut self, e: &UnaryExpr) {
401        match e.op {
402            op!("~")
403            | op!(unary, "-")
404            | op!(unary, "+")
405            | op!("!")
406            | op!("typeof")
407            | op!("void") => {
408                let ctx = self.ctx - Ctx::TrackExprIdent - Ctx::IsCallee;
409                e.visit_children_with(&mut *self.with_ctx(ctx));
410            }
411
412            _ => {
413                let ctx = (self.ctx | Ctx::TrackExprIdent) - Ctx::IsCallee;
414                e.visit_children_with(&mut *self.with_ctx(ctx));
415            }
416        }
417    }
418
419    fn visit_update_expr(&mut self, e: &UpdateExpr) {
420        let ctx = self.ctx - Ctx::TrackExprIdent - Ctx::IsCallee;
421        e.arg.visit_with(&mut *self.with_ctx(ctx));
422    }
423
424    fn visit_var_declarator(&mut self, n: &VarDeclarator) {
425        {
426            let old = self.ctx.contains(Ctx::IsPatDecl);
427            self.ctx.insert(Ctx::IsPatDecl);
428            n.name.visit_with(self);
429            self.ctx.set(Ctx::IsPatDecl, old);
430        }
431
432        if self.config.ignore_named_child_scope {
433            if let (Pat::Ident(..), Some(..)) = (&n.name, n.init.as_deref()) {
434                return;
435            }
436        }
437
438        {
439            let old = self.ctx.contains(Ctx::IsPatDecl);
440            self.ctx.remove(Ctx::IsPatDecl);
441            n.init.visit_with(self);
442            self.ctx.set(Ctx::IsPatDecl, old);
443        }
444    }
445}