Skip to main content

rspack_plugin_javascript/parser_plugin/
side_effects_parser_plugin.rs

1use std::sync::LazyLock;
2
3use rspack_core::{
4  DeferredPureCheck, Dependency, DependencyRange, ModuleDependency, SideEffectsBailoutItemWithSpan,
5};
6use rspack_util::SpanExt;
7use rustc_hash::FxHashSet;
8use swc_atoms::Atom;
9use swc_experimental_allocator::{CloneIn, atom::Atom as AstAtom};
10use swc_experimental_ecma_ast::{
11  ArrayLit, ArrowExpr, BlockStmt, BlockStmtOrExpr, CallExpr, Class, ClassMember, CommentKind,
12  Comments, Decl, DefaultDecl, ExportSpecifier, Expr, ExprOrSpread, Function, GetSpan,
13  ImportSpecifier, ModuleDecl, ModuleExportName, ModuleItem, ObjectPatProp, Pat, Program, PropName,
14  Span, Span as AstSpan, Stmt, VarDecl, VarDeclKind, VarDeclOrExpr, Visit, VisitWith,
15};
16use swc_experimental_ecma_utils::{ExprCtx, ExprExt};
17
18use crate::{
19  ClassExt, JavascriptParserPlugin,
20  dependency::ESMImportSideEffectDependency,
21  parser_plugin::esm_import_dependency_parser_plugin::{ESM_SPECIFIER_TAG, ESMSpecifierData},
22  visitors::{JavascriptParser, Statement, TagInfoData, VariableDeclaration},
23};
24
25static PURE_COMMENTS: LazyLock<regex::Regex> = LazyLock::new(|| {
26  regex::Regex::new("(?s)^\\s*(#|@)__PURE__(?:\\s|$)").expect("Should create the regex")
27});
28pub struct SideEffectsParserPlugin {
29  analyze_side_effects_free: bool,
30}
31
32impl SideEffectsParserPlugin {
33  pub fn new(analyze_side_effects_free: bool) -> Self {
34    Self {
35      analyze_side_effects_free,
36    }
37  }
38}
39
40struct PureAnnotation<'a> {
41  side_effects_free: FxHashSet<Atom>,
42  parser: &'a JavascriptParser<'a>,
43}
44
45fn compat_atom(atom: &AstAtom<'_>) -> Atom {
46  Atom::from(atom.as_str())
47}
48
49fn has_no_side_effects_notation(comments: &Comments<'_>, span: AstSpan) -> bool {
50  comments.has_flag(span.start, "NO_SIDE_EFFECTS")
51}
52
53fn expr_ctx<'a>(parser: &'a JavascriptParser<'_>, is_unresolved_ref_safe: bool) -> ExprCtx<'a> {
54  ExprCtx {
55    semantic: parser.ast.semantic,
56    is_unresolved_ref_safe,
57    in_strict: false,
58    remaining_depth: 4,
59  }
60}
61
62impl<'a> Visit<'a> for PureAnnotation<'a> {
63  fn visit_module_decl(&mut self, node: &ModuleDecl<'a>) {
64    match &node {
65      ModuleDecl::ExportDefaultExpr(default_expr) => {
66        if let Some(fn_expr) = default_expr.expr.as_fn()
67          && (has_no_side_effects_notation(self.parser.ast.comments, default_expr.span())
68            || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span()))
69        {
70          if let Some(ident) = &fn_expr.ident {
71            self.side_effects_free.insert(compat_atom(&ident.sym));
72          }
73          self.side_effects_free.insert(Atom::from("default"));
74        } else if let Some(arrow_expr) = default_expr.expr.as_arrow()
75          && (has_no_side_effects_notation(self.parser.ast.comments, default_expr.span())
76            || has_no_side_effects_notation(self.parser.ast.comments, arrow_expr.span()))
77        {
78          self.side_effects_free.insert(Atom::from("default"));
79        }
80      }
81      ModuleDecl::ExportDefaultDecl(default_decl) => {
82        if let Some(fn_expr) = default_decl.decl.as_fn()
83          && (has_no_side_effects_notation(self.parser.ast.comments, default_decl.span())
84            || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span()))
85        {
86          if let Some(ident) = &fn_expr.ident {
87            self.side_effects_free.insert(compat_atom(&ident.sym));
88          }
89          self.side_effects_free.insert(Atom::from("default"));
90        }
91      }
92      ModuleDecl::ExportDecl(export_decl) => {
93        if let Some(fn_decl) = export_decl.decl.as_fn()
94          && (has_no_side_effects_notation(self.parser.ast.comments, export_decl.span())
95            || has_no_side_effects_notation(self.parser.ast.comments, fn_decl.span()))
96        {
97          self
98            .side_effects_free
99            .insert(compat_atom(&fn_decl.ident.sym));
100        } else if let Some(var_decl) = export_decl.decl.as_var()
101          && matches!(var_decl.kind, VarDeclKind::Const)
102          && var_decl.decls.len() == 1
103        {
104          let const_decl = &var_decl.decls[0];
105          if let Some(ident) = const_decl.name.as_ident()
106            && let Some(Expr::Fn(fn_expr)) = const_decl.init.as_ref()
107            && (has_no_side_effects_notation(self.parser.ast.comments, var_decl.span())
108              || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span())
109              || has_no_side_effects_notation(self.parser.ast.comments, export_decl.span()))
110          {
111            self.side_effects_free.insert(compat_atom(&ident.id.sym));
112          } else if let Some(ident) = const_decl.name.as_ident()
113            && let Some(Expr::Arrow(fn_expr)) = const_decl.init.as_ref()
114            && (has_no_side_effects_notation(self.parser.ast.comments, var_decl.span())
115              || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span())
116              || has_no_side_effects_notation(self.parser.ast.comments, export_decl.span()))
117          {
118            self.side_effects_free.insert(compat_atom(&ident.id.sym));
119          }
120        }
121      }
122      _ => {}
123    }
124  }
125
126  fn visit_stmt(&mut self, node: &Stmt<'a>) {
127    if let Stmt::Decl(decl) = node {
128      #[allow(clippy::collapsible_match)]
129      match &**decl {
130        Decl::Fn(fn_decl) => {
131          if has_no_side_effects_notation(self.parser.ast.comments, fn_decl.span()) {
132            self
133              .side_effects_free
134              .insert(compat_atom(&fn_decl.ident.sym));
135          }
136        }
137        Decl::Var(var_decl) => {
138          /*
139          example:
140          ```
141          /*#__NO_SIDE_EFFECTS__*/ const sideEffectFreeVariable = () => {}
142          const sideEffectFreeVariable = /*#__NO_SIDE_EFFECTS__*/ () => {}
143          ```
144           */
145          if matches!(var_decl.kind, VarDeclKind::Const) && var_decl.decls.len() == 1 {
146            let const_decl = &var_decl.decls[0];
147
148            if let Some(ident) = const_decl.name.as_ident()
149              && let Some(Expr::Fn(fn_expr)) = const_decl.init.as_ref()
150              && (has_no_side_effects_notation(self.parser.ast.comments, var_decl.span())
151                || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span()))
152            {
153              self.side_effects_free.insert(compat_atom(&ident.id.sym));
154            } else if let Some(ident) = const_decl.name.as_ident()
155              && let Some(Expr::Arrow(fn_expr)) = const_decl.init.as_ref()
156              && (has_no_side_effects_notation(self.parser.ast.comments, var_decl.span())
157                || has_no_side_effects_notation(self.parser.ast.comments, fn_expr.span()))
158            {
159              self.side_effects_free.insert(compat_atom(&ident.id.sym));
160            }
161          }
162        }
163        _ => {}
164      }
165    }
166  }
167}
168
169fn collect_pure_function_acceptable_names(program: &Program) -> FxHashSet<Atom> {
170  // Names a user can list in `pureFunctions` and have actually take effect:
171  //   - any top-level binding (function/class/var decl or import) — so calls
172  //     to local helpers and imported identifiers can be marked pure;
173  //   - export aliases of local bindings (`export { foo as bar }`) and the
174  //     `default` keyword for default-exported functions/arrows — preserves
175  //     the original "configure on the source module" workflow.
176  let mut names = FxHashSet::default();
177  let mut insert = |name: Atom| {
178    names.insert(name);
179  };
180
181  match program {
182    Program::Module(module) => {
183      // First pass: collect every actual top-level binding name.
184      for item in &module.body {
185        match item {
186          ModuleItem::Stmt(stmt) => {
187            if let Stmt::Decl(decl) = &**stmt {
188              visit_decl_binding_names(decl, &mut insert)
189            }
190          }
191          ModuleItem::ModuleDecl(decl) => {
192            visit_module_decl_defined_binding_names(decl, &mut insert)
193          }
194        }
195      }
196
197      // Second pass: re-export aliases and `default` for default-exported fns.
198      let local_bindings = names.clone();
199      for item in &module.body {
200        let ModuleItem::ModuleDecl(decl) = item else {
201          continue;
202        };
203        match &**decl {
204          ModuleDecl::ExportNamed(named_export) if named_export.src.is_none() => {
205            for specifier in &named_export.specifiers {
206              let ExportSpecifier::Named(named) = specifier else {
207                continue;
208              };
209              let orig_atom = match &named.orig {
210                ModuleExportName::Ident(ident) => compat_atom(&ident.sym),
211                ModuleExportName::Str(_) => continue,
212              };
213              if !local_bindings.contains(&orig_atom) {
214                continue;
215              }
216              match named.exported.as_ref().unwrap_or(&named.orig) {
217                ModuleExportName::Ident(ident) => {
218                  names.insert(compat_atom(&ident.sym));
219                }
220                ModuleExportName::Str(s) => {
221                  names.insert(Atom::from(s.value.to_string_lossy().as_ref()));
222                }
223              }
224            }
225          }
226          ModuleDecl::ExportDefaultDecl(default_decl)
227            if matches!(default_decl.decl, DefaultDecl::Fn(_)) =>
228          {
229            names.insert(Atom::from("default"));
230          }
231          ModuleDecl::ExportDefaultExpr(default_expr)
232            if default_expr.expr.is_fn() || default_expr.expr.is_arrow() =>
233          {
234            names.insert(Atom::from("default"));
235          }
236          _ => {}
237        }
238      }
239    }
240    Program::Script(script) => {
241      for stmt in &script.body {
242        if let Stmt::Decl(decl) = stmt {
243          visit_decl_binding_names(decl, &mut insert);
244        }
245      }
246    }
247  }
248
249  names
250}
251
252fn collect_defined_configured_side_effects_free(
253  program: &Program,
254  configured_side_effects_free: &[String],
255) -> FxHashSet<Atom> {
256  let acceptable = collect_pure_function_acceptable_names(program);
257
258  configured_side_effects_free
259    .iter()
260    .filter_map(|name| {
261      let atom = Atom::from(name.clone());
262      acceptable.contains(&atom).then_some(atom)
263    })
264    .collect()
265}
266
267fn visit_pat_binding_names(pat: &Pat, f: &mut impl FnMut(Atom)) {
268  match pat {
269    Pat::Ident(ident) => f(compat_atom(&ident.id.sym)),
270    Pat::Array(array) => {
271      for elem in array.elems.iter().flatten() {
272        visit_pat_binding_names(elem, f);
273      }
274    }
275    Pat::Object(object) => {
276      for prop in &object.props {
277        match prop {
278          ObjectPatProp::KeyValue(prop) => {
279            visit_pat_binding_names(&prop.value, f);
280          }
281          ObjectPatProp::Assign(prop) => f(compat_atom(&prop.key.id.sym)),
282          ObjectPatProp::Rest(prop) => visit_pat_binding_names(&prop.arg, f),
283        }
284      }
285    }
286    Pat::Assign(assign) => visit_pat_binding_names(&assign.left, f),
287    Pat::Rest(rest) => visit_pat_binding_names(&rest.arg, f),
288    Pat::Expr(_) | Pat::Invalid(_) => {}
289  }
290}
291
292fn visit_decl_binding_names(decl: &Decl, f: &mut impl FnMut(Atom)) {
293  match decl {
294    Decl::Fn(fn_decl) => f(compat_atom(&fn_decl.ident.sym)),
295    Decl::Class(class_decl) => f(compat_atom(&class_decl.ident.sym)),
296    Decl::Var(var_decl) => {
297      for declarator in &var_decl.decls {
298        visit_pat_binding_names(&declarator.name, f);
299      }
300    }
301    _ => {}
302  }
303}
304
305fn visit_module_decl_defined_binding_names(decl: &ModuleDecl, f: &mut impl FnMut(Atom)) {
306  match decl {
307    ModuleDecl::Import(import_decl) => {
308      for specifier in &import_decl.specifiers {
309        match specifier {
310          ImportSpecifier::Named(named) => f(compat_atom(&named.local.sym)),
311          ImportSpecifier::Default(default) => f(compat_atom(&default.local.sym)),
312          ImportSpecifier::Namespace(namespace) => f(compat_atom(&namespace.local.sym)),
313        }
314      }
315    }
316    ModuleDecl::ExportDecl(export_decl) => visit_decl_binding_names(&export_decl.decl, f),
317    ModuleDecl::ExportDefaultDecl(default_decl) => match &default_decl.decl {
318      DefaultDecl::Fn(fn_expr) => {
319        if let Some(ident) = &fn_expr.ident {
320          f(compat_atom(&ident.sym));
321        }
322      }
323      DefaultDecl::Class(class_expr) => {
324        if let Some(ident) = &class_expr.ident {
325          f(compat_atom(&ident.sym));
326        }
327      }
328    },
329    _ => {}
330  }
331}
332
333fn collect_duplicate_top_level_names(program: &Program) -> FxHashSet<Atom> {
334  let mut counts = rustc_hash::FxHashMap::<Atom, usize>::default();
335  let mut count_name = |name: Atom| {
336    *counts.entry(name).or_default() += 1;
337  };
338
339  match program {
340    Program::Module(module) => {
341      for item in &module.body {
342        match item {
343          ModuleItem::Stmt(stmt) => {
344            if let Stmt::Decl(decl) = &**stmt {
345              visit_decl_binding_names(decl, &mut count_name)
346            }
347          }
348          ModuleItem::ModuleDecl(decl) => {
349            visit_module_decl_defined_binding_names(decl, &mut count_name)
350          }
351        }
352      }
353    }
354    Program::Script(script) => {
355      for stmt in &script.body {
356        if let Stmt::Decl(decl) = stmt {
357          visit_decl_binding_names(decl, &mut count_name);
358        }
359      }
360    }
361  }
362
363  counts
364    .into_iter()
365    .filter_map(|(name, count)| (count > 1).then_some(name))
366    .collect()
367}
368
369fn mark_side_effects_free(parser: &mut JavascriptParser, name: &Atom, export_name: Option<&Atom>) {
370  let side_effects_free = parser.build_info.side_effects_free.get_or_insert_default();
371  side_effects_free.insert(name.clone());
372  if let Some(export_name) = export_name {
373    side_effects_free.insert(export_name.clone());
374  }
375}
376
377fn try_mark_auto_side_effects_free_var_decl(
378  parser: &mut JavascriptParser,
379  analyze_side_effects_free: bool,
380  var_decl: &VarDecl,
381  export_name: Option<&Atom>,
382  comments: &Comments<'_>,
383  duplicate_names: &FxHashSet<Atom>,
384) {
385  if !matches!(var_decl.kind, VarDeclKind::Const) {
386    return;
387  }
388
389  for declarator in &var_decl.decls {
390    let Some(ident) = declarator.name.as_ident() else {
391      continue;
392    };
393    let ident = compat_atom(&ident.id.sym);
394
395    if parser
396      .build_info
397      .side_effects_free
398      .as_ref()
399      .is_some_and(|side_effects_free| side_effects_free.contains(&ident))
400    {
401      continue;
402    }
403
404    if duplicate_names.contains(&ident) {
405      continue;
406    }
407
408    let is_side_effects_free = match declarator.init.as_ref() {
409      Some(Expr::Fn(fn_expr)) => is_side_effects_free_function_body(
410        parser,
411        analyze_side_effects_free,
412        &fn_expr.function,
413        comments,
414      ),
415      Some(Expr::Arrow(arrow_expr)) => {
416        is_side_effects_free_arrow_body(parser, analyze_side_effects_free, arrow_expr, comments)
417      }
418      _ => false,
419    };
420
421    if is_side_effects_free {
422      mark_side_effects_free(parser, &ident, export_name);
423    }
424  }
425}
426
427fn try_mark_auto_side_effects_free_stmt(
428  parser: &mut JavascriptParser,
429  analyze_side_effects_free: bool,
430  stmt: &Stmt,
431  comments: &Comments<'_>,
432  duplicate_names: &FxHashSet<Atom>,
433) {
434  if let Stmt::Decl(decl) = stmt {
435    match &**decl {
436      Decl::Fn(fn_decl) => {
437        let ident = compat_atom(&fn_decl.ident.sym);
438        if parser
439          .build_info
440          .side_effects_free
441          .as_ref()
442          .is_some_and(|side_effects_free| side_effects_free.contains(&ident))
443          || duplicate_names.contains(&ident)
444        {
445          return;
446        }
447
448        if is_side_effects_free_function_body(
449          parser,
450          analyze_side_effects_free,
451          &fn_decl.function,
452          comments,
453        ) {
454          mark_side_effects_free(parser, &ident, None);
455        }
456      }
457      Decl::Var(var_decl) => try_mark_auto_side_effects_free_var_decl(
458        parser,
459        analyze_side_effects_free,
460        var_decl,
461        None,
462        comments,
463        duplicate_names,
464      ),
465      _ => {}
466    }
467  }
468}
469
470fn try_mark_auto_side_effects_free_module_decl(
471  parser: &mut JavascriptParser,
472  analyze_side_effects_free: bool,
473  decl: &ModuleDecl,
474  comments: &Comments<'_>,
475  duplicate_names: &FxHashSet<Atom>,
476) {
477  match decl {
478    ModuleDecl::ExportDefaultExpr(default_expr) => {
479      let Some(fn_expr) = default_expr.expr.as_fn() else {
480        return;
481      };
482      let Some(ident) = &fn_expr.ident else {
483        return;
484      };
485      let ident = compat_atom(&ident.sym);
486      let export_name = Atom::from("default");
487      if parser
488        .build_info
489        .side_effects_free
490        .as_ref()
491        .is_some_and(|side_effects_free| side_effects_free.contains(&ident))
492        || duplicate_names.contains(&ident)
493      {
494        return;
495      }
496      if is_side_effects_free_function_body(
497        parser,
498        analyze_side_effects_free,
499        &fn_expr.function,
500        comments,
501      ) {
502        mark_side_effects_free(parser, &ident, Some(&export_name));
503      }
504    }
505    ModuleDecl::ExportDefaultDecl(default_decl) => {
506      let Some(fn_expr) = default_decl.decl.as_fn() else {
507        return;
508      };
509      let Some(ident) = &fn_expr.ident else {
510        return;
511      };
512      let ident = compat_atom(&ident.sym);
513      let export_name = Atom::from("default");
514      if parser
515        .build_info
516        .side_effects_free
517        .as_ref()
518        .is_some_and(|side_effects_free| side_effects_free.contains(&ident))
519        || duplicate_names.contains(&ident)
520      {
521        return;
522      }
523      if is_side_effects_free_function_body(
524        parser,
525        analyze_side_effects_free,
526        &fn_expr.function,
527        comments,
528      ) {
529        mark_side_effects_free(parser, &ident, Some(&export_name));
530      }
531    }
532    ModuleDecl::ExportDecl(export_decl) => match &export_decl.decl {
533      Decl::Fn(fn_decl) => {
534        if parser
535          .build_info
536          .side_effects_free
537          .as_ref()
538          .is_some_and(|side_effects_free| {
539            side_effects_free.contains(&compat_atom(&fn_decl.ident.sym))
540          })
541          || duplicate_names.contains(&compat_atom(&fn_decl.ident.sym))
542        {
543          return;
544        }
545
546        if is_side_effects_free_function_body(
547          parser,
548          analyze_side_effects_free,
549          &fn_decl.function,
550          comments,
551        ) {
552          mark_side_effects_free(parser, &compat_atom(&fn_decl.ident.sym), None);
553        }
554      }
555      Decl::Var(var_decl) => try_mark_auto_side_effects_free_var_decl(
556        parser,
557        analyze_side_effects_free,
558        var_decl,
559        None,
560        comments,
561        duplicate_names,
562      ),
563      _ => {}
564    },
565    _ => {}
566  }
567}
568
569fn mark_auto_side_effects_free_program(
570  parser: &mut JavascriptParser,
571  analyze_side_effects_free: bool,
572  program: &Program,
573  comments: &Comments<'_>,
574  duplicate_names: &FxHashSet<Atom>,
575) {
576  match program {
577    Program::Module(module) => {
578      for item in &module.body {
579        match item {
580          ModuleItem::Stmt(stmt) => try_mark_auto_side_effects_free_stmt(
581            parser,
582            analyze_side_effects_free,
583            stmt,
584            comments,
585            duplicate_names,
586          ),
587          ModuleItem::ModuleDecl(decl) => try_mark_auto_side_effects_free_module_decl(
588            parser,
589            analyze_side_effects_free,
590            decl,
591            comments,
592            duplicate_names,
593          ),
594        }
595      }
596    }
597    Program::Script(script) => {
598      for stmt in &script.body {
599        try_mark_auto_side_effects_free_stmt(
600          parser,
601          analyze_side_effects_free,
602          stmt,
603          comments,
604          duplicate_names,
605        );
606      }
607    }
608  }
609}
610
611#[rspack_macros::implemented_javascript_parser_hooks]
612impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for SideEffectsParserPlugin {
613  fn program(&self, parser: &mut JavascriptParser<'p>, ast: &Program) -> Option<bool> {
614    parser.build_info.side_effects_free = None;
615    parser.build_info.deferred_pure_checks.clear();
616
617    // analyze if any function contains #__NO_SIDE_EFFECTS__ annotation
618    // so that pure functions in current module can be marked as pure
619    if self.analyze_side_effects_free {
620      // Build the explicit/implicit pureFunctions set in three steps:
621      // 1. collect NO_SIDE_EFFECTS annotations already present in the module,
622      // 2. keep only configured names that actually exist at top level,
623      // 3. run a fixed-point top-level auto analysis so local pure helpers can
624      //    unlock later candidates regardless of declaration order.
625      // use a raw swc visitor so that we can find all pure functions before the parser visit the ast
626      let mut pure_annotation = PureAnnotation {
627        side_effects_free: FxHashSet::default(),
628        parser,
629      };
630      ast.visit_with(&mut pure_annotation);
631      let detected_side_effects_free = pure_annotation.side_effects_free;
632      if !detected_side_effects_free.is_empty() {
633        let side_effects_free = parser.build_info.side_effects_free.get_or_insert_default();
634        side_effects_free.extend(detected_side_effects_free);
635      }
636
637      if let Some(flagged_side_effects_free) = &parser.javascript_options.side_effects_free {
638        let defined_side_effects_free =
639          collect_defined_configured_side_effects_free(ast, flagged_side_effects_free);
640        if !defined_side_effects_free.is_empty() {
641          let side_effects_free = parser.build_info.side_effects_free.get_or_insert_default();
642          side_effects_free.extend(defined_side_effects_free);
643        }
644      }
645
646      let duplicate_names = collect_duplicate_top_level_names(ast);
647      loop {
648        let prev_len = parser
649          .build_info
650          .side_effects_free
651          .as_ref()
652          .map_or(0, FxHashSet::len);
653        mark_auto_side_effects_free_program(
654          parser,
655          self.analyze_side_effects_free,
656          ast,
657          parser.ast.comments,
658          &duplicate_names,
659        );
660        let next_len = parser
661          .build_info
662          .side_effects_free
663          .as_ref()
664          .map_or(0, FxHashSet::len);
665        if next_len == prev_len {
666          break;
667        }
668      }
669    }
670
671    None
672  }
673
674  fn module_declaration(
675    &self,
676    parser: &mut JavascriptParser<'p>,
677    decl: &ModuleDecl,
678  ) -> Option<bool> {
679    match decl {
680      ModuleDecl::ExportDefaultExpr(expr) => {
681        let mut callees = vec![];
682        if !is_pure_expression(
683          parser,
684          self.analyze_side_effects_free,
685          &expr.expr,
686          parser.ast.comments,
687          Some(&mut callees),
688        ) {
689          let range = DependencyRange::from(expr.span);
690          let loc = parser.to_dependency_location(range);
691          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
692            range,
693            loc,
694            String::from("ExportDefaultExpr"),
695          ));
696        } else {
697          // record all potential pure callee
698          for (callee, span) in callees {
699            if let Some(deferred_check) = try_extract_deferred_check(parser, callee, span) {
700              parser
701                .build_info
702                .deferred_pure_checks
703                .insert(deferred_check);
704            } else {
705              let range = DependencyRange::from(span);
706              let loc = parser.to_dependency_location(range);
707              parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
708                range,
709                loc,
710                String::from("ExportDefaultExpr"),
711              ));
712              break;
713            }
714          }
715        }
716      }
717      ModuleDecl::ExportDecl(decl) => {
718        let mut callees = vec![];
719        if !is_pure_decl(
720          parser,
721          self.analyze_side_effects_free,
722          &decl.decl,
723          parser.ast.comments,
724          Some(&mut callees),
725        ) {
726          let range = DependencyRange::from(decl.decl.span());
727          let loc = parser.to_dependency_location(range);
728          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
729            range,
730            loc,
731            String::from("Decl"),
732          ));
733        }
734        for (callee, span) in callees {
735          if let Some(deferred_check) = try_extract_deferred_check(parser, callee, span) {
736            parser
737              .build_info
738              .deferred_pure_checks
739              .insert(deferred_check);
740          } else {
741            let range = DependencyRange::from(span);
742            let loc = parser.to_dependency_location(range);
743            parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
744              range,
745              loc,
746              String::from("Decl"),
747            ));
748            break;
749          }
750        }
751      }
752      _ => {}
753    };
754    None
755  }
756  fn statement(&self, parser: &mut JavascriptParser<'p>, stmt: Statement) -> Option<bool> {
757    if !parser.is_top_level_scope() {
758      return None;
759    }
760    self.analyze_stmt_side_effects(&stmt, parser);
761    None
762  }
763
764  fn finish(&self, parser: &mut JavascriptParser<'p>) -> Option<bool> {
765    if self.analyze_side_effects_free {
766      let mut not_defined = Vec::new();
767      // check if all user flagged side_effects_free are defined
768      if let Some(side_effects_free) = &parser.javascript_options.side_effects_free {
769        let mut side_effects_free = side_effects_free.iter().collect::<Vec<_>>();
770        side_effects_free.sort();
771        let defined_side_effects_free = parser.build_info.side_effects_free.as_ref();
772        for atom in side_effects_free {
773          if !defined_side_effects_free.is_some_and(|configured_side_effects_free| {
774            configured_side_effects_free.contains(&Atom::from(atom.clone()))
775          }) {
776            not_defined.push(Atom::from(atom.clone()));
777          }
778        }
779      }
780
781      if !not_defined.is_empty() {
782        if let Some(side_effects_free) = parser.build_info.side_effects_free.as_mut() {
783          for atom in &not_defined {
784            side_effects_free.remove(atom);
785          }
786        }
787
788        let resource = parser.resource_data.resource();
789        parser.add_warning(
790          rspack_error::Diagnostic::warn("PURE_FUNCTION_NOT_FOUND".into(), format!("Following pure functions are not found in {resource}:\n[{}]\nRemove it from `module.rules[*].parser.pureFunctions`", not_defined.iter().map(|atom| format!("`{atom}`")).collect::<Vec<_>>().join(", ")))
791        );
792      }
793    }
794
795    None
796  }
797}
798
799#[inline(never)]
800fn is_pure_call_expr(
801  parser: &mut JavascriptParser,
802  analyze_side_effects_free: bool,
803  expr: &Expr,
804  comments: &Comments<'_>,
805  callees: Option<&mut Vec<(Atom, Span)>>,
806) -> bool {
807  let Expr::Call(call_expr) = expr else {
808    unreachable!();
809  };
810  let pure_flag = has_pure_comment(comments, expr.span().start)
811    || has_pure_comment(comments, call_expr.callee.span().start);
812  let callee = &call_expr.callee;
813
814  if pure_flag {
815    return is_pure_call_args(
816      parser,
817      analyze_side_effects_free,
818      call_expr,
819      comments,
820      callees,
821    );
822  } else if analyze_side_effects_free && let Some(Expr::Ident(ident)) = callee.as_expr() {
823    match resolve_explicit_side_effects_free_callee(
824      parser,
825      &compat_atom(&ident.sym),
826      callee.span(),
827      callees.is_none(),
828    ) {
829      ExplicitSideEffectsFreeCallee::Direct => {
830        return is_pure_call_args(
831          parser,
832          analyze_side_effects_free,
833          call_expr,
834          comments,
835          callees,
836        );
837      }
838      ExplicitSideEffectsFreeCallee::Deferred => {
839        let Some(callees) = callees else {
840          return false;
841        };
842        callees.push((compat_atom(&ident.sym), callee.span()));
843        return is_pure_call_args(
844          parser,
845          analyze_side_effects_free,
846          call_expr,
847          comments,
848          Some(callees),
849        );
850      }
851      ExplicitSideEffectsFreeCallee::Invalid => return false,
852      ExplicitSideEffectsFreeCallee::NotMarked => {}
853    }
854
855    if let Some(callees) = callees {
856      callees.push((compat_atom(&ident.sym), callee.span()));
857      return is_pure_call_args(
858        parser,
859        analyze_side_effects_free,
860        call_expr,
861        comments,
862        Some(callees),
863      );
864    }
865  }
866
867  !expr.may_have_side_effects(expr_ctx(parser, false))
868}
869
870#[inline(never)]
871fn is_pure_call_args(
872  parser: &mut JavascriptParser,
873  analyze_side_effects_free: bool,
874  call_expr: &CallExpr,
875  comments: &Comments<'_>,
876  mut callees: Option<&mut Vec<(Atom, Span)>>,
877) -> bool {
878  for arg in &call_expr.args {
879    if arg.spread.is_some() {
880      return false;
881    }
882    if !is_pure_expression(
883      parser,
884      analyze_side_effects_free,
885      &arg.expr,
886      comments,
887      callees.as_deref_mut(),
888    ) {
889      return false;
890    }
891  }
892  true
893}
894
895fn is_pure_array_lit<'a>(
896  parser: &mut JavascriptParser,
897  analyze_side_effects_free: bool,
898  array_lit: &'a ArrayLit,
899  comments: &'a Comments<'a>,
900  mut callees: Option<&mut Vec<(Atom, Span)>>,
901) -> bool {
902  for elem in array_lit.elems.iter().flatten() {
903    if elem.spread.is_some()
904      || !is_pure_expression(
905        parser,
906        analyze_side_effects_free,
907        &elem.expr,
908        comments,
909        callees.as_deref_mut(),
910      )
911    {
912      return false;
913    }
914  }
915  true
916}
917
918enum ExplicitSideEffectsFreeCallee {
919  Direct,
920  Deferred,
921  Invalid,
922  NotMarked,
923}
924
925fn resolve_explicit_side_effects_free_callee(
926  parser: &mut JavascriptParser,
927  ident: &Atom,
928  span: Span,
929  allow_unresolved_marked: bool,
930) -> ExplicitSideEffectsFreeCallee {
931  let is_marked = parser
932    .build_info
933    .side_effects_free
934    .as_ref()
935    .is_some_and(|side_effects_free| side_effects_free.contains(ident));
936
937  if !is_marked {
938    return ExplicitSideEffectsFreeCallee::NotMarked;
939  }
940
941  // For non-imports the deferred path doesn't apply at all, so we skip the
942  // user-config lookup and fall straight through to Direct/Invalid.
943  if try_extract_deferred_check(parser, ident.clone(), span).is_some() {
944    // When the user explicitly listed this name in `pureFunctions`, trust the
945    // assertion at the call site instead of deferring to the import target.
946    // This lets users mark imported helpers (e.g. `import { cva } from 'cva'`)
947    // as pure on the consumer side without also configuring the source module.
948    let is_user_configured = parser
949      .javascript_options
950      .side_effects_free
951      .as_ref()
952      .is_some_and(|names| names.iter().any(|name| name.as_str() == ident.as_str()));
953    if !is_user_configured {
954      return ExplicitSideEffectsFreeCallee::Deferred;
955    }
956  }
957
958  if let Some((declared_scope, is_free)) = parser
959    .get_variable_info(ident)
960    .map(|info| (info.declared_scope, info.is_free()))
961  {
962    if !is_free && declared_scope == parser.definitions {
963      return ExplicitSideEffectsFreeCallee::Direct;
964    }
965    return ExplicitSideEffectsFreeCallee::Invalid;
966  }
967
968  if allow_unresolved_marked {
969    return ExplicitSideEffectsFreeCallee::Direct;
970  }
971
972  ExplicitSideEffectsFreeCallee::Invalid
973}
974
975fn try_extract_deferred_check(
976  parser: &mut JavascriptParser,
977  ident: Atom,
978  span: Span,
979) -> Option<DeferredPureCheck> {
980  let info = parser.get_variable_info(&ident)?;
981
982  let tag_info_id = info.tag_info?;
983  let tag_info = parser.definitions_db.expect_get_tag_info(tag_info_id);
984
985  if tag_info.tag != ESM_SPECIFIER_TAG {
986    return None;
987  }
988
989  let data = ESMSpecifierData::downcast(tag_info.data.clone()?);
990
991  parser
992    .get_dependencies()
993    .iter()
994    .find(|dep| {
995      let Some(dep) = dep.downcast_ref::<ESMImportSideEffectDependency>() else {
996        return false;
997      };
998
999      let request_eq = dep.request() == &data.source;
1000      let attributes: Option<&rspack_core::ImportAttributes> = data.attributes.as_ref();
1001      let attributes_eq = attributes == dep.get_attributes();
1002      request_eq && attributes_eq
1003    })
1004    .map(|dep| DeferredPureCheck {
1005      atom: data
1006        .ids
1007        .first()
1008        .cloned()
1009        .unwrap_or_else(|| data.name.clone()),
1010      dep_id: *dep.id(),
1011      start: span.real_lo(),
1012      end: span.real_hi(),
1013    })
1014}
1015
1016fn is_pure_new_expr(
1017  parser: &mut JavascriptParser,
1018  analyze_side_effects_free: bool,
1019  expr: &Expr,
1020  comments: &Comments<'_>,
1021) -> bool {
1022  let Expr::New(new_expr) = expr else {
1023    unreachable!();
1024  };
1025  let pure_flag = has_pure_comment(comments, expr.span().start);
1026  if !pure_flag {
1027    !expr.may_have_side_effects(expr_ctx(parser, false))
1028  } else {
1029    are_pure_args(
1030      parser,
1031      analyze_side_effects_free,
1032      new_expr.args.as_deref().unwrap_or(&[]),
1033      comments,
1034    )
1035  }
1036}
1037
1038fn has_pure_comment(comments: &Comments<'_>, pos: u32) -> bool {
1039  comments.leading.get(&pos).is_some_and(|comment_list| {
1040    comment_list
1041      .iter()
1042      .any(|comment| comment.kind == CommentKind::Block && PURE_COMMENTS.is_match(&comment.text))
1043  })
1044}
1045
1046fn are_pure_args<'a>(
1047  parser: &mut JavascriptParser,
1048  analyze_side_effects_free: bool,
1049  args: &'a [ExprOrSpread],
1050  comments: &'a Comments<'a>,
1051) -> bool {
1052  args.iter().all(|arg| {
1053    if arg.spread.is_some() {
1054      false
1055    } else {
1056      is_pure_expression(parser, analyze_side_effects_free, &arg.expr, comments, None)
1057    }
1058  })
1059}
1060
1061impl SideEffectsParserPlugin {
1062  fn analyze_stmt_side_effects(&self, stmt: &Statement, parser: &mut JavascriptParser) {
1063    if parser.side_effects_item.is_some() {
1064      return;
1065    }
1066    let mut callees = vec![];
1067    match stmt {
1068      Statement::If(if_stmt) => {
1069        if !is_pure_expression(
1070          parser,
1071          self.analyze_side_effects_free,
1072          &if_stmt.test,
1073          parser.ast.comments,
1074          Some(&mut callees),
1075        ) {
1076          let range = DependencyRange::from(if_stmt.span());
1077          let loc = parser.to_dependency_location(range);
1078          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1079            range,
1080            loc,
1081            String::from("Statement"),
1082          ));
1083        }
1084      }
1085      Statement::While(while_stmt) => {
1086        if !is_pure_expression(
1087          parser,
1088          self.analyze_side_effects_free,
1089          &while_stmt.test,
1090          parser.ast.comments,
1091          Some(&mut callees),
1092        ) {
1093          let range = DependencyRange::from(while_stmt.span());
1094          let loc = parser.to_dependency_location(range);
1095          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1096            range,
1097            loc,
1098            String::from("Statement"),
1099          ));
1100        }
1101      }
1102      Statement::DoWhile(do_while_stmt) => {
1103        if !is_pure_expression(
1104          parser,
1105          self.analyze_side_effects_free,
1106          &do_while_stmt.test,
1107          parser.ast.comments,
1108          Some(&mut callees),
1109        ) {
1110          let range = DependencyRange::from(do_while_stmt.span());
1111          let loc = parser.to_dependency_location(range);
1112          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1113            range,
1114            loc,
1115            String::from("Statement"),
1116          ));
1117        }
1118      }
1119      Statement::For(for_stmt) => {
1120        let pure_init = match for_stmt.init {
1121          Some(ref init) => match init {
1122            VarDeclOrExpr::VarDecl(decl) => is_pure_var_decl(
1123              parser,
1124              self.analyze_side_effects_free,
1125              decl,
1126              parser.ast.comments,
1127              Some(&mut callees),
1128            ),
1129            VarDeclOrExpr::Expr(expr) => is_pure_expression(
1130              parser,
1131              self.analyze_side_effects_free,
1132              expr,
1133              parser.ast.comments,
1134              Some(&mut callees),
1135            ),
1136          },
1137          None => true,
1138        };
1139
1140        if !pure_init {
1141          let range = DependencyRange::from(for_stmt.span());
1142          let loc = parser.to_dependency_location(range);
1143          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1144            range,
1145            loc,
1146            String::from("Statement"),
1147          ));
1148          return;
1149        }
1150
1151        let pure_test = match &for_stmt.test {
1152          Some(test) => is_pure_expression(
1153            parser,
1154            self.analyze_side_effects_free,
1155            test,
1156            parser.ast.comments,
1157            Some(&mut callees),
1158          ),
1159          None => true,
1160        };
1161
1162        if !pure_test {
1163          let range = DependencyRange::from(for_stmt.span());
1164          let loc = parser.to_dependency_location(range);
1165          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1166            range,
1167            loc,
1168            String::from("Statement"),
1169          ));
1170          return;
1171        }
1172
1173        let pure_update = match for_stmt.update {
1174          Some(ref expr) => is_pure_expression(
1175            parser,
1176            self.analyze_side_effects_free,
1177            expr,
1178            parser.ast.comments,
1179            Some(&mut callees),
1180          ),
1181          None => true,
1182        };
1183
1184        if !pure_update {
1185          let range = DependencyRange::from(for_stmt.span());
1186          let loc = parser.to_dependency_location(range);
1187          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1188            range,
1189            loc,
1190            String::from("Statement"),
1191          ));
1192        }
1193      }
1194      Statement::Expr(expr_stmt) => {
1195        if !is_pure_expression(
1196          parser,
1197          self.analyze_side_effects_free,
1198          &expr_stmt.expr,
1199          parser.ast.comments,
1200          Some(&mut callees),
1201        ) {
1202          let range = DependencyRange::from(expr_stmt.span());
1203          let loc = parser.to_dependency_location(range);
1204          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1205            range,
1206            loc,
1207            String::from("Statement"),
1208          ));
1209        }
1210      }
1211      Statement::Switch(switch_stmt) => {
1212        if !is_pure_expression(
1213          parser,
1214          self.analyze_side_effects_free,
1215          &switch_stmt.discriminant,
1216          parser.ast.comments,
1217          Some(&mut callees),
1218        ) {
1219          let range = DependencyRange::from(switch_stmt.span());
1220          let loc = parser.to_dependency_location(range);
1221          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1222            range,
1223            loc,
1224            String::from("Statement"),
1225          ));
1226        }
1227      }
1228      Statement::Class(class_stmt) => {
1229        if !is_pure_class(
1230          parser,
1231          self.analyze_side_effects_free,
1232          class_stmt.class(),
1233          parser.ast.comments,
1234          Some(&mut callees),
1235        ) {
1236          let range = DependencyRange::from(stmt.span());
1237          let loc = parser.to_dependency_location(range);
1238          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1239            range,
1240            loc,
1241            String::from("Statement"),
1242          ));
1243        }
1244      }
1245      Statement::Var(var_stmt) => match var_stmt {
1246        VariableDeclaration::VarDecl(var_decl) => {
1247          if !is_pure_var_decl(
1248            parser,
1249            self.analyze_side_effects_free,
1250            var_decl,
1251            parser.ast.comments,
1252            Some(&mut callees),
1253          ) {
1254            let range = DependencyRange::from(var_stmt.span());
1255            let loc = parser.to_dependency_location(range);
1256            parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1257              range,
1258              loc,
1259              String::from("Statement"),
1260            ));
1261          }
1262        }
1263        VariableDeclaration::UsingDecl(_) => {
1264          let range = DependencyRange::from(var_stmt.span());
1265          let loc = parser.to_dependency_location(range);
1266          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1267            range,
1268            loc,
1269            String::from("Statement"),
1270          ));
1271        }
1272      },
1273      Statement::Empty(_) => {}
1274      Statement::Labeled(_) => {}
1275      Statement::Block(_) => {}
1276      Statement::Fn(_) => {}
1277      _ => {
1278        let range = DependencyRange::from(stmt.span());
1279        let loc = parser.to_dependency_location(range);
1280        parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1281          range,
1282          loc,
1283          String::from("Statement"),
1284        ))
1285      }
1286    };
1287
1288    if parser.side_effects_item.is_none() {
1289      for (callee, span) in callees {
1290        if let Some(deferred_check) = try_extract_deferred_check(parser, callee, span) {
1291          parser
1292            .build_info
1293            .deferred_pure_checks
1294            .insert(deferred_check);
1295        } else {
1296          let range = DependencyRange::from(span);
1297          let loc = parser.to_dependency_location(range);
1298          parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
1299            range,
1300            loc,
1301            String::from("Statement"),
1302          ));
1303          break;
1304        }
1305      }
1306    }
1307  }
1308}
1309
1310pub fn is_pure_pat<'a>(
1311  parser: &mut JavascriptParser,
1312  analyze_side_effects_free: bool,
1313  pat: &'a Pat,
1314  comments: &'a Comments<'a>,
1315  mut callees: Option<&mut Vec<(Atom, Span)>>,
1316) -> bool {
1317  match pat {
1318    Pat::Ident(_) => true,
1319    Pat::Array(array_pat) => {
1320      for pat in array_pat.elems.iter().flatten() {
1321        if !is_pure_pat(
1322          parser,
1323          analyze_side_effects_free,
1324          pat,
1325          comments,
1326          callees.as_deref_mut(),
1327        ) {
1328          return false;
1329        }
1330      }
1331      true
1332    }
1333    Pat::Rest(_) => true,
1334    Pat::Invalid(_) | Pat::Assign(_) | Pat::Object(_) => false,
1335    Pat::Expr(expr) => {
1336      is_pure_expression(parser, analyze_side_effects_free, expr, comments, callees)
1337    }
1338  }
1339}
1340
1341fn is_side_effects_free_param(pat: &Pat) -> bool {
1342  matches!(pat, Pat::Ident(_))
1343}
1344
1345#[inline(never)]
1346fn is_side_effects_free_var_decl(
1347  parser: &mut JavascriptParser,
1348  analyze_side_effects_free: bool,
1349  var_decl: &VarDecl,
1350  comments: &Comments<'_>,
1351) -> bool {
1352  for declarator in &var_decl.decls {
1353    if declarator.name.as_ident().is_none() {
1354      return false;
1355    }
1356
1357    if let Some(init) = declarator.init.as_ref()
1358      && !is_pure_expression(parser, analyze_side_effects_free, init, comments, None)
1359    {
1360      return false;
1361    }
1362  }
1363
1364  true
1365}
1366
1367fn stmt_may_have_side_effects(parser: &JavascriptParser, stmt: &Stmt) -> bool {
1368  let expr_ctx = expr_ctx(parser, true);
1369
1370  match stmt {
1371    Stmt::Empty(_) => false,
1372    Stmt::Expr(expr_stmt) => expr_stmt.expr.may_have_side_effects(expr_ctx),
1373    Stmt::Return(return_stmt) => return_stmt
1374      .arg
1375      .as_ref()
1376      .is_some_and(|arg| arg.may_have_side_effects(expr_ctx)),
1377    Stmt::Decl(decl) => match &**decl {
1378      Decl::Var(var_decl) => var_decl.decls.iter().any(|declarator| {
1379        declarator.name.as_ident().is_none()
1380          || declarator
1381            .init
1382            .as_ref()
1383            .is_some_and(|init| init.may_have_side_effects(expr_ctx))
1384      }),
1385      _ => true,
1386    },
1387    _ => true,
1388  }
1389}
1390
1391fn is_side_effects_free_stmt(
1392  parser: &mut JavascriptParser,
1393  analyze_side_effects_free: bool,
1394  stmt: &Stmt,
1395  comments: &Comments<'_>,
1396) -> bool {
1397  if !stmt_may_have_side_effects(parser, stmt) {
1398    return true;
1399  }
1400
1401  match stmt {
1402    Stmt::Expr(expr_stmt) => is_pure_expression(
1403      parser,
1404      analyze_side_effects_free,
1405      &expr_stmt.expr,
1406      comments,
1407      None,
1408    ),
1409    Stmt::Return(return_stmt) => return_stmt
1410      .arg
1411      .as_ref()
1412      .is_none_or(|arg| is_pure_expression(parser, analyze_side_effects_free, arg, comments, None)),
1413    Stmt::Decl(decl) => match &**decl {
1414      Decl::Var(var_decl) => {
1415        is_side_effects_free_var_decl(parser, analyze_side_effects_free, var_decl, comments)
1416      }
1417      _ => false,
1418    },
1419    _ => false,
1420  }
1421}
1422
1423#[inline(never)]
1424fn is_side_effects_free_block_stmt(
1425  parser: &mut JavascriptParser,
1426  analyze_side_effects_free: bool,
1427  block_stmt: &BlockStmt,
1428  comments: &Comments<'_>,
1429) -> bool {
1430  for stmt in &block_stmt.stmts {
1431    if !is_side_effects_free_stmt(parser, analyze_side_effects_free, stmt, comments) {
1432      return false;
1433    }
1434  }
1435
1436  true
1437}
1438
1439#[inline(never)]
1440fn is_side_effects_free_function_body(
1441  parser: &mut JavascriptParser,
1442  analyze_side_effects_free: bool,
1443  function: &Function,
1444  comments: &Comments<'_>,
1445) -> bool {
1446  if !function
1447    .params
1448    .iter()
1449    .all(|param| is_side_effects_free_param(&param.pat))
1450  {
1451    return false;
1452  }
1453
1454  function.body.as_ref().is_none_or(|body| {
1455    is_side_effects_free_block_stmt(parser, analyze_side_effects_free, body, comments)
1456  })
1457}
1458
1459#[inline(never)]
1460fn is_side_effects_free_arrow_body(
1461  parser: &mut JavascriptParser,
1462  analyze_side_effects_free: bool,
1463  arrow_expr: &ArrowExpr,
1464  comments: &Comments<'_>,
1465) -> bool {
1466  if !arrow_expr.params.iter().all(is_side_effects_free_param) {
1467    return false;
1468  }
1469
1470  match &arrow_expr.body {
1471    BlockStmtOrExpr::BlockStmt(block_stmt) => {
1472      is_side_effects_free_block_stmt(parser, analyze_side_effects_free, block_stmt, comments)
1473    }
1474    BlockStmtOrExpr::Expr(expr) => {
1475      is_pure_expression(parser, analyze_side_effects_free, expr, comments, None)
1476    }
1477  }
1478}
1479
1480pub fn is_pure_function<'a>(
1481  parser: &mut JavascriptParser,
1482  analyze_side_effects_free: bool,
1483  function: &'a Function,
1484  comments: &'a Comments<'a>,
1485  mut callees: Option<&mut Vec<(Atom, Span)>>,
1486) -> bool {
1487  for param in &function.params {
1488    if !is_pure_pat(
1489      parser,
1490      analyze_side_effects_free,
1491      &param.pat,
1492      comments,
1493      callees.as_deref_mut(),
1494    ) {
1495      return false;
1496    }
1497  }
1498  true
1499}
1500
1501#[inline(never)]
1502pub fn is_pure_expression<'a>(
1503  parser: &mut JavascriptParser,
1504  analyze_side_effects_free: bool,
1505  expr: &'a Expr,
1506  comments: &'a Comments<'a>,
1507  callees: Option<&mut Vec<(Atom, Span)>>,
1508) -> bool {
1509  pub fn _is_pure_expression<'a>(
1510    parser: &mut JavascriptParser,
1511    analyze_side_effects_free: bool,
1512    expr: &'a Expr,
1513    comments: &'a Comments<'a>,
1514    mut callees: Option<&mut Vec<(Atom, Span)>>,
1515  ) -> bool {
1516    if let Some(res) = parser.plugin_drive.clone().is_pure(parser, expr) {
1517      return res;
1518    }
1519
1520    match expr {
1521      Expr::Array(array_lit) => is_pure_array_lit(
1522        parser,
1523        analyze_side_effects_free,
1524        array_lit,
1525        comments,
1526        callees.as_deref_mut(),
1527      ),
1528      Expr::Call(_) => {
1529        is_pure_call_expr(parser, analyze_side_effects_free, expr, comments, callees)
1530      }
1531      Expr::New(_) => is_pure_new_expr(parser, analyze_side_effects_free, expr, comments),
1532      Expr::Paren(_) => unreachable!(),
1533      Expr::Seq(seq_expr) => {
1534        for expr in &seq_expr.exprs {
1535          if !is_pure_expression(
1536            parser,
1537            analyze_side_effects_free,
1538            expr,
1539            comments,
1540            callees.as_deref_mut(),
1541          ) {
1542            return false;
1543          }
1544        }
1545        true
1546      }
1547      _ => {
1548        if !expr.may_have_side_effects(expr_ctx(parser, true)) {
1549          return true;
1550        }
1551        // could_have_side_effects is true by default, so here we test if it's modified by other plugins to return false.
1552        let evaluated = parser.evaluate_expression(expr);
1553        !evaluated.could_have_side_effects()
1554      }
1555    }
1556  }
1557  _is_pure_expression(parser, analyze_side_effects_free, expr, comments, callees)
1558}
1559
1560#[inline(never)]
1561pub fn is_pure_class_member<'a>(
1562  parser: &mut JavascriptParser,
1563  analyze_side_effects_free: bool,
1564  member: &'a ClassMember,
1565  comments: &'a Comments<'a>,
1566  mut callees: Option<&mut Vec<(Atom, Span)>>,
1567) -> bool {
1568  let is_key_pure = match member.class_key() {
1569    Some(PropName::Ident(_ident)) => true,
1570    Some(PropName::Str(_)) => true,
1571    Some(PropName::Num(_)) => true,
1572    Some(PropName::Computed(computed)) => is_pure_expression(
1573      parser,
1574      analyze_side_effects_free,
1575      &computed.expr,
1576      comments,
1577      callees.as_deref_mut(),
1578    ),
1579    Some(PropName::BigInt(_)) => true,
1580    None => true,
1581  };
1582  if !is_key_pure {
1583    return false;
1584  }
1585  let is_static = member.is_static();
1586  let is_value_pure = match member {
1587    ClassMember::Constructor(_) => true,
1588    ClassMember::Method(_) => true,
1589    ClassMember::PrivateMethod(_) => true,
1590    ClassMember::ClassProp(prop) => {
1591      if let Some(ref value) = prop.value {
1592        is_pure_expression(
1593          parser,
1594          analyze_side_effects_free,
1595          value,
1596          comments,
1597          callees.as_deref_mut(),
1598        )
1599      } else {
1600        true
1601      }
1602    }
1603    ClassMember::PrivateProp(prop) => {
1604      if let Some(ref value) = prop.value {
1605        is_pure_expression(parser, analyze_side_effects_free, value, comments, callees)
1606      } else {
1607        true
1608      }
1609    }
1610    ClassMember::Empty(_) => true,
1611    ClassMember::StaticBlock(_) => false,
1612    ClassMember::AutoAccessor(_) => false,
1613  };
1614  if is_static && !is_value_pure {
1615    return false;
1616  }
1617  true
1618}
1619
1620#[inline(never)]
1621pub fn is_pure_decl(
1622  parser: &mut JavascriptParser,
1623  analyze_side_effects_free: bool,
1624  stmt: &Decl,
1625  comments: &Comments<'_>,
1626  callees: Option<&mut Vec<(Atom, Span)>>,
1627) -> bool {
1628  match stmt {
1629    Decl::Class(class) => is_pure_class(
1630      parser,
1631      analyze_side_effects_free,
1632      &class.class,
1633      comments,
1634      callees,
1635    ),
1636    Decl::Fn(_) => true,
1637    Decl::Var(var) => is_pure_var_decl(parser, analyze_side_effects_free, var, comments, callees),
1638    Decl::Using(_) => false,
1639  }
1640}
1641
1642#[inline(never)]
1643pub fn is_pure_class(
1644  parser: &mut JavascriptParser,
1645  analyze_side_effects_free: bool,
1646  class: &Class,
1647  comments: &Comments<'_>,
1648  mut callees: Option<&mut Vec<(Atom, Span)>>,
1649) -> bool {
1650  if let Some(ref super_class) = class.super_class
1651    && !is_pure_expression(
1652      parser,
1653      analyze_side_effects_free,
1654      super_class,
1655      comments,
1656      callees.as_deref_mut(),
1657    )
1658  {
1659    return false;
1660  }
1661  let is_pure_key = |parser: &mut JavascriptParser,
1662                     key: &PropName,
1663                     callees: Option<&mut Vec<(Atom, Span)>>|
1664   -> bool {
1665    match key {
1666      PropName::BigInt(_) | PropName::Ident(_) | PropName::Str(_) | PropName::Num(_) => true,
1667      PropName::Computed(computed) => is_pure_expression(
1668        parser,
1669        analyze_side_effects_free,
1670        &computed.expr,
1671        comments,
1672        callees,
1673      ),
1674    }
1675  };
1676
1677  for item in &class.body {
1678    let pure = match item {
1679      ClassMember::Constructor(_) => class.super_class.is_none(),
1680      ClassMember::Method(method) => is_pure_key(parser, &method.key, callees.as_deref_mut()),
1681      ClassMember::PrivateMethod(method) => is_pure_expression(
1682        parser,
1683        analyze_side_effects_free,
1684        &Expr::PrivateName(method.key.clone_in(parser.ast.allocator)),
1685        comments,
1686        callees.as_deref_mut(),
1687      ),
1688      ClassMember::ClassProp(prop) => {
1689        is_pure_key(parser, &prop.key, callees.as_deref_mut())
1690          && (!prop.is_static
1691            || if let Some(ref value) = prop.value {
1692              is_pure_expression(
1693                parser,
1694                analyze_side_effects_free,
1695                value,
1696                comments,
1697                callees.as_deref_mut(),
1698              )
1699            } else {
1700              true
1701            })
1702      }
1703      ClassMember::PrivateProp(prop) => {
1704        is_pure_expression(
1705          parser,
1706          analyze_side_effects_free,
1707          &Expr::PrivateName(prop.key.clone_in(parser.ast.allocator)),
1708          comments,
1709          callees.as_deref_mut(),
1710        ) && (!prop.is_static
1711          || if let Some(ref value) = prop.value {
1712            is_pure_expression(
1713              parser,
1714              analyze_side_effects_free,
1715              value,
1716              comments,
1717              callees.as_deref_mut(),
1718            )
1719          } else {
1720            true
1721          })
1722      }
1723      ClassMember::Empty(_) => true,
1724      ClassMember::StaticBlock(_) => false, // TODO: support is pure analyze for statements
1725      ClassMember::AutoAccessor(_) => false,
1726    };
1727    if !pure {
1728      return false;
1729    }
1730  }
1731  true
1732}
1733
1734#[inline(never)]
1735fn is_pure_var_decl<'a>(
1736  parser: &mut JavascriptParser,
1737  analyze_side_effects_free: bool,
1738  var: &'a VarDecl,
1739  comments: &'a Comments<'a>,
1740  mut callees: Option<&mut Vec<(Atom, Span)>>,
1741) -> bool {
1742  for decl in &var.decls {
1743    if let Some(ref init) = decl.init
1744      && !is_pure_expression(
1745        parser,
1746        analyze_side_effects_free,
1747        init,
1748        comments,
1749        callees.as_deref_mut(),
1750      )
1751    {
1752      return false;
1753    }
1754  }
1755  true
1756}