rspack_plugin_javascript/parser_plugin/
side_effects_parser_plugin.rs

1use std::sync::LazyLock;
2
3use rspack_core::SideEffectsBailoutItemWithSpan;
4use swc_core::{
5  common::{
6    Mark, Spanned, SyntaxContext,
7    comments::{CommentKind, Comments},
8  },
9  ecma::{
10    ast::{
11      Class, ClassMember, Decl, Expr, Function, ModuleDecl, Pat, PropName, VarDecl, VarDeclOrExpr,
12    },
13    utils::{ExprCtx, ExprExt},
14  },
15};
16
17use crate::{
18  ClassExt, JavascriptParserPlugin,
19  visitors::{JavascriptParser, Statement, VariableDeclaration},
20};
21
22static PURE_COMMENTS: LazyLock<regex::Regex> =
23  LazyLock::new(|| regex::Regex::new("^\\s*(#|@)__PURE__\\s*$").expect("Should create the regex"));
24
25pub struct SideEffectsParserPlugin {
26  unresolve_ctxt: SyntaxContext,
27}
28
29impl SideEffectsParserPlugin {
30  pub fn new(unresolved_mark: Mark) -> Self {
31    Self {
32      unresolve_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
33    }
34  }
35}
36
37impl JavascriptParserPlugin for SideEffectsParserPlugin {
38  fn module_declaration(&self, parser: &mut JavascriptParser, decl: &ModuleDecl) -> Option<bool> {
39    match decl {
40      ModuleDecl::ExportDefaultExpr(expr) => {
41        if !is_pure_expression(parser, &expr.expr, self.unresolve_ctxt, parser.comments) {
42          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
43            expr.span,
44            String::from("ExportDefaultExpr"),
45          ));
46        }
47      }
48      ModuleDecl::ExportDecl(decl) => {
49        if !is_pure_decl(parser, &decl.decl, self.unresolve_ctxt, parser.comments) {
50          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
51            decl.decl.span(),
52            String::from("Decl"),
53          ));
54        }
55      }
56      _ => {}
57    };
58    None
59  }
60  fn statement(&self, parser: &mut JavascriptParser, stmt: Statement) -> Option<bool> {
61    if !parser.is_top_level_scope() {
62      return None;
63    }
64    self.analyze_stmt_side_effects(&stmt, parser);
65    None
66  }
67}
68
69fn is_pure_call_expr(
70  parser: &mut JavascriptParser,
71  expr: &Expr,
72  unresolved_ctxt: SyntaxContext,
73  comments: Option<&dyn Comments>,
74) -> bool {
75  let Expr::Call(call_expr) = expr else {
76    unreachable!();
77  };
78  let callee = &call_expr.callee;
79  let pure_flag = comments
80    .and_then(|comments| {
81      if let Some(comment_list) = comments.get_leading(callee.span().lo) {
82        return Some(comment_list.iter().any(|comment| {
83          comment.kind == CommentKind::Block && PURE_COMMENTS.is_match(&comment.text)
84        }));
85      }
86      None
87    })
88    .unwrap_or(false);
89  if !pure_flag {
90    !expr.may_have_side_effects(ExprCtx {
91      unresolved_ctxt,
92      in_strict: false,
93      is_unresolved_ref_safe: false,
94      remaining_depth: 4,
95    })
96  } else {
97    call_expr.args.iter().all(|arg| {
98      if arg.spread.is_some() {
99        false
100      } else {
101        is_pure_expression(parser, &arg.expr, unresolved_ctxt, comments)
102      }
103    })
104  }
105}
106
107impl SideEffectsParserPlugin {
108  fn analyze_stmt_side_effects(&self, stmt: &Statement, parser: &mut JavascriptParser) {
109    if parser.side_effects_item.is_some() {
110      return;
111    }
112    match stmt {
113      Statement::If(if_stmt) => {
114        if !is_pure_expression(parser, &if_stmt.test, self.unresolve_ctxt, parser.comments) {
115          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
116            if_stmt.span(),
117            String::from("Statement"),
118          ));
119        }
120      }
121      Statement::While(while_stmt) => {
122        if !is_pure_expression(
123          parser,
124          &while_stmt.test,
125          self.unresolve_ctxt,
126          parser.comments,
127        ) {
128          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
129            while_stmt.span(),
130            String::from("Statement"),
131          ));
132        }
133      }
134      Statement::DoWhile(do_while_stmt) => {
135        if !is_pure_expression(
136          parser,
137          &do_while_stmt.test,
138          self.unresolve_ctxt,
139          parser.comments,
140        ) {
141          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
142            do_while_stmt.span(),
143            String::from("Statement"),
144          ));
145        }
146      }
147      Statement::For(for_stmt) => {
148        let pure_init = match for_stmt.init {
149          Some(ref init) => match init {
150            VarDeclOrExpr::VarDecl(decl) => {
151              is_pure_var_decl(parser, decl, self.unresolve_ctxt, parser.comments)
152            }
153            VarDeclOrExpr::Expr(expr) => {
154              is_pure_expression(parser, expr, self.unresolve_ctxt, parser.comments)
155            }
156          },
157          None => true,
158        };
159
160        if !pure_init {
161          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
162            for_stmt.span(),
163            String::from("Statement"),
164          ));
165          return;
166        }
167
168        let pure_test = match &for_stmt.test {
169          Some(test) => is_pure_expression(parser, test, self.unresolve_ctxt, parser.comments),
170          None => true,
171        };
172
173        if !pure_test {
174          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
175            for_stmt.span(),
176            String::from("Statement"),
177          ));
178          return;
179        }
180
181        let pure_update = match for_stmt.update {
182          Some(ref expr) => is_pure_expression(parser, expr, self.unresolve_ctxt, parser.comments),
183          None => true,
184        };
185
186        if !pure_update {
187          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
188            for_stmt.span(),
189            String::from("Statement"),
190          ));
191        }
192      }
193      Statement::Expr(expr_stmt) => {
194        if !is_pure_expression(
195          parser,
196          &expr_stmt.expr,
197          self.unresolve_ctxt,
198          parser.comments,
199        ) {
200          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
201            expr_stmt.span(),
202            String::from("Statement"),
203          ));
204        }
205      }
206      Statement::Switch(switch_stmt) => {
207        if !is_pure_expression(
208          parser,
209          &switch_stmt.discriminant,
210          self.unresolve_ctxt,
211          parser.comments,
212        ) {
213          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
214            switch_stmt.span(),
215            String::from("Statement"),
216          ));
217        }
218      }
219      Statement::Class(class_stmt) => {
220        if !is_pure_class(
221          parser,
222          class_stmt.class(),
223          self.unresolve_ctxt,
224          parser.comments,
225        ) {
226          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
227            class_stmt.span(),
228            String::from("Statement"),
229          ));
230        }
231      }
232      Statement::Var(var_stmt) => match var_stmt {
233        VariableDeclaration::VarDecl(var_decl) => {
234          if !is_pure_var_decl(parser, var_decl, self.unresolve_ctxt, parser.comments) {
235            parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
236              var_stmt.span(),
237              String::from("Statement"),
238            ));
239          }
240        }
241        VariableDeclaration::UsingDecl(_) => {
242          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
243            var_stmt.span(),
244            String::from("Statement"),
245          ));
246        }
247      },
248      Statement::Empty(_) => {}
249      Statement::Labeled(_) => {}
250      Statement::Block(_) => {}
251      Statement::Fn(_) => {}
252      _ => {
253        parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
254          stmt.span(),
255          String::from("Statement"),
256        ))
257      }
258    };
259  }
260}
261
262pub fn is_pure_pat<'a>(
263  parser: &mut JavascriptParser,
264  pat: &'a Pat,
265  unresolved_ctxt: SyntaxContext,
266  comments: Option<&'a dyn Comments>,
267) -> bool {
268  match pat {
269    Pat::Ident(_) => true,
270    Pat::Array(array_pat) => array_pat.elems.iter().all(|ele| {
271      if let Some(pat) = ele {
272        is_pure_pat(parser, pat, unresolved_ctxt, comments)
273      } else {
274        true
275      }
276    }),
277    Pat::Rest(_) => true,
278    Pat::Invalid(_) | Pat::Assign(_) | Pat::Object(_) => false,
279    Pat::Expr(expr) => is_pure_expression(parser, expr, unresolved_ctxt, comments),
280  }
281}
282
283pub fn is_pure_function<'a>(
284  parser: &mut JavascriptParser,
285  function: &'a Function,
286  unresolved_ctxt: SyntaxContext,
287  comments: Option<&'a dyn Comments>,
288) -> bool {
289  if !function
290    .params
291    .iter()
292    .all(|param| is_pure_pat(parser, &param.pat, unresolved_ctxt, comments))
293  {
294    return false;
295  }
296
297  true
298}
299
300pub fn is_pure_expression<'a>(
301  parser: &mut JavascriptParser,
302  expr: &'a Expr,
303  unresolved_ctxt: SyntaxContext,
304  comments: Option<&'a dyn Comments>,
305) -> bool {
306  pub fn _is_pure_expression<'a>(
307    parser: &mut JavascriptParser,
308    expr: &'a Expr,
309    unresolved_ctxt: SyntaxContext,
310    comments: Option<&'a dyn Comments>,
311  ) -> bool {
312    let drive = parser.plugin_drive.clone();
313    if let Some(res) = drive.is_pure(parser, expr) {
314      return res;
315    }
316
317    match expr {
318      Expr::Call(_) => is_pure_call_expr(parser, expr, unresolved_ctxt, comments),
319      Expr::Paren(_) => unreachable!(),
320      Expr::Seq(seq_expr) => seq_expr
321        .exprs
322        .iter()
323        .all(|expr| is_pure_expression(parser, expr, unresolved_ctxt, comments)),
324      _ => !expr.may_have_side_effects(ExprCtx {
325        unresolved_ctxt,
326        is_unresolved_ref_safe: true,
327        in_strict: false,
328        remaining_depth: 4,
329      }),
330    }
331  }
332  _is_pure_expression(parser, expr, unresolved_ctxt, comments)
333}
334
335pub fn is_pure_class_member<'a>(
336  parser: &mut JavascriptParser,
337  member: &'a ClassMember,
338  unresolved_ctxt: SyntaxContext,
339  comments: Option<&'a dyn Comments>,
340) -> bool {
341  let is_key_pure = match member.class_key() {
342    Some(PropName::Ident(_ident)) => true,
343    Some(PropName::Str(_)) => true,
344    Some(PropName::Num(_)) => true,
345    Some(PropName::Computed(computed)) => {
346      is_pure_expression(parser, &computed.expr, unresolved_ctxt, comments)
347    }
348    Some(PropName::BigInt(_)) => true,
349    None => true,
350  };
351  if !is_key_pure {
352    return false;
353  }
354  let is_static = member.is_static();
355  let is_value_pure = match member {
356    ClassMember::Constructor(_) => true,
357    ClassMember::Method(_) => true,
358    ClassMember::PrivateMethod(_) => true,
359    ClassMember::ClassProp(prop) => {
360      if let Some(ref value) = prop.value {
361        is_pure_expression(parser, value, unresolved_ctxt, comments)
362      } else {
363        true
364      }
365    }
366    ClassMember::PrivateProp(prop) => {
367      if let Some(ref value) = prop.value {
368        is_pure_expression(parser, value, unresolved_ctxt, comments)
369      } else {
370        true
371      }
372    }
373    ClassMember::TsIndexSignature(_) => unreachable!(),
374    ClassMember::Empty(_) => true,
375    ClassMember::StaticBlock(_) => false,
376    ClassMember::AutoAccessor(_) => false,
377  };
378  if is_static && !is_value_pure {
379    return false;
380  }
381  true
382}
383
384pub fn is_pure_decl(
385  parser: &mut JavascriptParser,
386  stmt: &Decl,
387  unresolved_ctxt: SyntaxContext,
388  comments: Option<&dyn Comments>,
389) -> bool {
390  match stmt {
391    Decl::Class(class) => is_pure_class(parser, &class.class, unresolved_ctxt, comments),
392    Decl::Fn(_) => true,
393    Decl::Var(var) => is_pure_var_decl(parser, var, unresolved_ctxt, comments),
394    Decl::Using(_) => false,
395    Decl::TsInterface(_) => unreachable!(),
396    Decl::TsTypeAlias(_) => unreachable!(),
397
398    Decl::TsEnum(_) => unreachable!(),
399    Decl::TsModule(_) => unreachable!(),
400  }
401}
402
403pub fn is_pure_class(
404  parser: &mut JavascriptParser,
405  class: &Class,
406  unresolved_ctxt: SyntaxContext,
407  comments: Option<&dyn Comments>,
408) -> bool {
409  if let Some(ref super_class) = class.super_class
410    && !is_pure_expression(parser, super_class, unresolved_ctxt, comments)
411  {
412    return false;
413  }
414  let is_pure_key = |parser: &mut JavascriptParser, key: &PropName| -> bool {
415    match key {
416      PropName::BigInt(_) | PropName::Ident(_) | PropName::Str(_) | PropName::Num(_) => true,
417      PropName::Computed(computed) => {
418        is_pure_expression(parser, &computed.expr, unresolved_ctxt, comments)
419      }
420    }
421  };
422
423  class.body.iter().all(|item| -> bool {
424    match item {
425      ClassMember::Constructor(_) => class.super_class.is_none(),
426      ClassMember::Method(method) => is_pure_key(parser, &method.key),
427      ClassMember::PrivateMethod(method) => is_pure_expression(
428        parser,
429        &Expr::PrivateName(method.key.clone()),
430        unresolved_ctxt,
431        comments,
432      ),
433      ClassMember::ClassProp(prop) => {
434        is_pure_key(parser, &prop.key)
435          && (!prop.is_static
436            || if let Some(ref value) = prop.value {
437              is_pure_expression(parser, value, unresolved_ctxt, comments)
438            } else {
439              true
440            })
441      }
442      ClassMember::PrivateProp(prop) => {
443        is_pure_expression(
444          parser,
445          &Expr::PrivateName(prop.key.clone()),
446          unresolved_ctxt,
447          comments,
448        ) && (!prop.is_static
449          || if let Some(ref value) = prop.value {
450            is_pure_expression(parser, value, unresolved_ctxt, comments)
451          } else {
452            true
453          })
454      }
455      ClassMember::TsIndexSignature(_) => unreachable!(),
456      ClassMember::Empty(_) => true,
457      ClassMember::StaticBlock(_) => true,
458      ClassMember::AutoAccessor(_) => true,
459    }
460  })
461}
462
463fn is_pure_var_decl<'a>(
464  parser: &mut JavascriptParser,
465  var: &'a VarDecl,
466  unresolved_ctxt: SyntaxContext,
467  comments: Option<&'a dyn Comments>,
468) -> bool {
469  var.decls.iter().all(|decl| {
470    if let Some(ref init) = decl.init {
471      is_pure_expression(parser, init, unresolved_ctxt, comments)
472    } else {
473      true
474    }
475  })
476}