swc_ecma_usage_analyzer/alias/
mod.rs1#![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 pub need_all: bool,
21
22 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
118pub 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}