Skip to main content

turbopack_ecmascript/analyzer/
side_effects.rs

1//! Side effect analysis for JavaScript/TypeScript programs.
2//!
3//! This module provides functionality to determine if a javascript script/module has side effects
4//! during module evaluation. This is useful for tree-shaking and dead code elimination.
5//!
6//! ## What are side effects?
7//!
8//! A side effect is any observable behavior that occurs when code is executed:
9//! - Function calls (unless marked with `/*#__PURE__*/` or otherwise known to be pure)
10//! - Constructor calls (unless marked with `/*#__PURE__*/`or otherwise known to be pure )
11//! - Assignments to variables or properties
12//! - Property mutations
13//! - Update expressions (`++`, `--`)
14//! - Delete expressions
15//!
16//! ## Conservative Analysis
17//!
18//! This analyzer is intentionally conservative. When in doubt, it assumes code
19//! has side effects. This is safe for tree-shaking purposes as it prevents
20//! incorrectly removing code that might be needed, and can simply be improved over time.
21//!
22//! ## Local Variable Mutation Tracking
23//!
24//! Currently, assignments to local unaliased constants and `module.exports` are considered
25//! side-effect free. This handles the common pattern:
26//!
27//! ```javascript
28//! // Currently marked as having side effects, but could be pure:
29//! const config = {};
30//! config['a'] = 'a';
31//! config['b'] = 'b';
32//! export default config;
33//! ```
34//!
35//! All other assignments, updates, and property mutations are currently treated as side effects.
36//! In the future, it would be good to explore non-constant variables. However, this is more
37//! challenging as they can be aliased after being initialised.
38
39use std::collections::HashSet;
40
41use phf::{phf_map, phf_set};
42use swc_core::{
43    common::{Mark, comments::Comments},
44    ecma::{
45        ast::*,
46        utils::prop_name_eq,
47        visit::{Visit, VisitWith, noop_visit_type},
48    },
49};
50use turbopack_core::module::ModuleSideEffects;
51
52use crate::{
53    analyzer::cjs_ast::{
54        as_exports_define_property, is_cjs_export_member, is_global, is_module_dot_exports,
55        is_module_exports_chain,
56    },
57    utils::unparen,
58};
59
60/// Macro to check if side effects have been detected and return early if so.
61/// This makes the early-return pattern more explicit and reduces boilerplate.
62macro_rules! check_side_effects {
63    ($self:expr) => {
64        if $self.has_side_effects {
65            return;
66        }
67    };
68}
69
70/// Known pure built-in functions organized by object (e.g., Math, Object, Array).
71///
72/// These are JavaScript built-in functions that are known to be side-effect free.
73/// This list is conservative and only includes functions that:
74/// 1. Don't modify global state
75/// 2. Don't perform I/O
76/// 3. Are deterministic (given the same inputs, produce the same outputs)
77///
78/// Note: Some of these can throw exceptions, but for tree-shaking purposes,
79/// we consider them pure as they don't have observable side effects beyond exceptions.
80static KNOWN_PURE_FUNCTIONS: phf::Map<&'static str, phf::Set<&'static str>> = phf_map! {
81    "Math" => phf_set! {
82        "abs", "acos", "acosh", "asin", "asinh", "atan", "atan2", "atanh", "cbrt", "ceil",
83        "clz32", "cos", "cosh", "exp", "expm1", "floor", "fround", "hypot", "imul", "log",
84        "log10", "log1p", "log2", "max", "min", "pow", "round", "sign", "sin", "sinh",
85        "sqrt", "tan", "tanh", "trunc",
86    },
87    // String static methods
88    "String" => phf_set! {
89        "fromCharCode", "fromCodePoint", "raw",
90    },
91    // Number static methods
92    "Number" => phf_set! {
93        "isFinite", "isInteger", "isNaN", "isSafeInteger", "parseFloat", "parseInt",
94    },
95    // Object static methods (read-only operations)
96    "Object" => phf_set! {
97        "keys", "values", "entries", "hasOwn", "getOwnPropertyNames", "getOwnPropertySymbols",
98        "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", "getPrototypeOf", "is",
99        "isExtensible", "isFrozen", "isSealed",
100    },
101    // Array static methods
102    "Array" => phf_set! {
103        "isArray", "from", "of",
104    },
105    // Symbol static methods
106    "Symbol" => phf_set! {
107        "for", "keyFor"
108    },
109};
110
111/// Known pure global functions that can be called directly (not as methods).
112///
113/// These are global functions that are side-effect free when called.
114/// Structured as phf::Set for O(1) lookup.
115static KNOWN_PURE_GLOBAL_FUNCTIONS: phf::Set<&'static str> = phf_set! {
116    "String",
117    "Number",
118    "Symbol",
119    "Boolean",
120    "isNaN",
121    "isFinite",
122    "parseInt",
123    "parseFloat",
124    "decodeURI",
125    "decodeURIComponent",
126};
127
128/// Known pure constructors.
129///
130/// These constructors create new objects without side effects (no global state modification).
131/// They are safe to eliminate if their result is unused.
132static KNOWN_PURE_CONSTRUCTORS: phf::Set<&'static str> = phf_set! {
133    // Built-in collections
134    "Set",
135    "Map",
136    "WeakSet",
137    "WeakMap",
138    // Regular expressions
139    "RegExp",
140    // Data structures
141    "Array",
142    "Object",
143    // Typed arrays
144    "Int8Array",
145    "Uint8Array",
146    "Uint8ClampedArray",
147    "Int16Array",
148    "Uint16Array",
149    "Int32Array",
150    "Uint32Array",
151    "Float32Array",
152    "Float64Array",
153    "BigInt64Array",
154    "BigUint64Array",
155    // Other built-ins
156    "Date",
157    "Error",
158    "TypeError",
159    "RangeError",
160    "SyntaxError",
161    "ReferenceError",
162    "URIError",
163    "EvalError",
164    "Promise",
165    "ArrayBuffer",
166    "DataView",
167    "URL",
168    "URLSearchParams",
169    // Boxes
170    "String",
171    "Number",
172    "Symbol",
173    "Boolean",
174};
175
176// For prototype methods we are not saying that these functions are always side effect free but
177// rather that we can safely reason about their side effects when called on literal expressions.
178// We do however assume that these functions are not monkey patched.
179
180/// Known pure prototype methods for string literals.
181///
182/// These methods don't mutate the string (strings are immutable) and don't have side effects.
183static KNOWN_PURE_STRING_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! {
184    // Case conversion
185    "toLowerCase",
186    "toUpperCase",
187    "toLocaleLowerCase",
188    "toLocaleUpperCase",
189    "charAt",
190    "charCodeAt",
191    "codePointAt",
192    "slice",
193    "substring",
194    "substr",
195    "indexOf",
196    "lastIndexOf",
197    "includes",
198    "startsWith",
199    "endsWith",
200    "search",
201    "match",
202    "matchAll",
203    "trim",
204    "trimStart",
205    "trimEnd",
206    "trimLeft",
207    "trimRight",
208    "repeat",
209    "padStart",
210    "padEnd",
211    "concat",
212    "split",
213    "replace",
214    "replaceAll",
215    "normalize",
216    "localeCompare",
217    "isWellFormed",
218    "toString",
219    "valueOf",
220};
221
222/// Known pure prototype methods for array literals.
223static KNOWN_PURE_ARRAY_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! {
224    // Non-mutating iteration
225    "map",
226    "filter",
227    "reduce",
228    "reduceRight",
229    "find",
230    "findIndex",
231    "findLast",
232    "findLastIndex",
233    "some",
234    "every",
235    "flat",
236    "flatMap",
237    // Access methods
238    "at",
239    "slice",
240    "concat",
241    "includes",
242    "indexOf",
243    "lastIndexOf",
244    "join",
245    // Conversion
246    "toLocaleString",
247    "toReversed",
248    "toSorted",
249    "toSpliced",
250    "with",
251};
252
253static KNOWN_PURE_OBJECT_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! {
254    "hasOwnProperty",
255    "propertyIsEnumerable",
256    "toString",
257    "valueOf",
258};
259
260/// Known pure prototype methods for number literals.
261static KNOWN_PURE_NUMBER_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! {
262    "toExponential", "toFixed", "toPrecision", "toLocaleString",
263};
264
265/// Known pure prototype methods for RegExp literals.
266///
267/// Note: While `test()` and `exec()` mutate `lastIndex` on regexes with global/sticky flags,
268/// for literal regexes this is safe because:
269/// 1. Literals create fresh objects each time
270/// 2. The mutation is local to that object
271/// 3. The mutated state doesn't escape the expression
272///
273/// However, to be conservative for tree-shaking, we exclude these methods.
274static KNOWN_PURE_REGEXP_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! {
275    "test", "exec",
276};
277
278/// A freshly-allocated object/array literal (evaluates to a brand-new value).
279fn is_object_or_array_literal(expr: &Expr) -> bool {
280    matches!(unparen(expr), Expr::Object(_) | Expr::Array(_))
281}
282
283/// Returns the root identifier of an `a.b.c`-style assignment target, e.g.
284/// `a.b.c` -> `a`, or `None` if the base isn't a plain identifier.
285fn root_identifier(expr: &Expr) -> Option<&Ident> {
286    match unparen(expr) {
287        Expr::Ident(ident) => Some(ident),
288        Expr::Member(member) => root_identifier(&member.obj),
289        _ => None,
290    }
291}
292
293/// Collects `const` bindings initialized with an object/array literal that have
294/// no accessor. `const c = importedObj` would be filtered out — its initializer
295/// is an  identifier, not a literal. This is to prevent us from marking
296/// assignments to aliased variables as side-effect free. For example:
297///
298/// ```javascript
299/// const c = globalThis;
300/// c.fetch = sideEffects();
301/// ```
302///
303/// Has side-effects.
304fn collect_safe_assignment_constant_ids(program: &Program) -> HashSet<Id> {
305    // Collect `const` bindings initialized to a fresh, accessor-free literal.
306    // Function/method bodies are skipped: a binding declared there can't be the
307    // root of an assignment that runs during module evaluation.
308    struct Collector {
309        ids: HashSet<Id>,
310    }
311    impl Visit for Collector {
312        noop_visit_type!();
313        fn visit_var_decl(&mut self, decl: &VarDecl) {
314            if decl.kind == VarDeclKind::Const {
315                for d in &decl.decls {
316                    if let (Pat::Ident(binding), Some(init)) = (&d.name, d.init.as_deref())
317                        && is_object_or_array_literal(init)
318                        && is_fresh_value(init)
319                        && !contains_getters_or_setters(init)
320                    {
321                        self.ids.insert(binding.id.to_id());
322                    }
323                }
324            }
325            decl.visit_children_with(self);
326        }
327        fn visit_function(&mut self, _: &Function) {}
328        fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
329    }
330    let mut collector = Collector {
331        ids: HashSet::new(),
332    };
333    program.visit_with(&mut collector);
334    let mut ids = collector.ids;
335
336    // Drop any binding that later has an accessor attached to its object graph
337    // (e.g. `o.x = { set y(v) {} }`): a subsequent write through that property
338    // could invoke the accessor, so the binding is no longer safe to mutate.
339    // The same goes for a value the module doesn't own (`o.x = globalThis`).
340    for_each_top_level_expr(program, |expr| {
341        if let Expr::Assign(assign) = expr
342            && assign.op == AssignOp::Assign
343            && let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assign.left
344            && (!is_fresh_value(&assign.right) || contains_getters_or_setters(&assign.right))
345            && let Some(root) = root_identifier(&member.obj)
346        {
347            ids.remove(&root.to_id());
348        }
349    });
350
351    ids
352}
353
354/// Whether `expr` is a primitive or an object created by the expression itself,
355/// transitively. `{ g: globalThis }` is not: `box.g.x = 1` writes to the global.
356fn is_fresh_value(expr: &Expr) -> bool {
357    match unparen(expr) {
358        Expr::Lit(_) | Expr::Tpl(_) | Expr::Fn(_) | Expr::Arrow(_) => true,
359        Expr::Object(obj) => obj.props.iter().all(|prop| match prop {
360            PropOrSpread::Prop(prop) => match &**prop {
361                Prop::KeyValue(kv) => is_fresh_value(&kv.value),
362                Prop::Method(_) => true,
363                Prop::Shorthand(_) | Prop::Getter(_) | Prop::Setter(_) | Prop::Assign(_) => false,
364            },
365            PropOrSpread::Spread(spread) => is_fresh_value(&spread.expr),
366        }),
367        Expr::Array(arr) => arr
368            .elems
369            .iter()
370            .flatten()
371            .all(|elem| is_fresh_value(&elem.expr)),
372        _ => false,
373    }
374}
375
376/// Whether `expr`'s object graph contains a getter or setter. An accessor makes
377/// member access (read *or* write) potentially effectful — e.g. `o.foo = 1`
378/// invokes a `set foo` — so a value carrying one can't be attached to the
379/// exports object and then mutated as if it were plain data. Function/method
380/// bodies are not descended into: an accessor declared inside a nested function
381/// isn't part of this value's own shape.
382fn contains_getters_or_setters(expr: &Expr) -> bool {
383    match unparen(expr) {
384        Expr::Object(obj) => obj.props.iter().any(|prop| match prop {
385            PropOrSpread::Prop(prop) => match &**prop {
386                Prop::Getter(_) | Prop::Setter(_) => true,
387                Prop::KeyValue(kv) => contains_getters_or_setters(&kv.value),
388                Prop::Method(_) | Prop::Shorthand(_) | Prop::Assign(_) => false,
389            },
390            PropOrSpread::Spread(spread) => contains_getters_or_setters(&spread.expr),
391        }),
392        Expr::Array(arr) => arr
393            .elems
394            .iter()
395            .flatten()
396            .any(|elem| contains_getters_or_setters(&elem.expr)),
397        _ => false,
398    }
399}
400
401/// Calls `f` for every expression that executes during module evaluation.
402///
403/// This descends through all expressions — conditionals, logical/binary
404/// operators, sequences, assignment chains, call arguments, etc. — so an
405/// assignment hidden in `cond && (module.exports = …)` or `a ? (b = …) : c` is
406/// still seen. It does *not* descend into function/method bodies: those don't
407/// run at module-evaluation time (calling such a function would itself be a side
408/// effect), so assignments inside them are irrelevant here.
409fn for_each_top_level_expr(program: &Program, f: impl FnMut(&Expr)) {
410    struct Collector<F> {
411        f: F,
412    }
413    impl<F: FnMut(&Expr)> Visit for Collector<F> {
414        noop_visit_type!();
415        fn visit_expr(&mut self, n: &Expr) {
416            (self.f)(n);
417            n.visit_children_with(self);
418        }
419        // Function/method bodies do not execute during module evaluation.
420        fn visit_function(&mut self, _: &Function) {}
421        fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
422        fn visit_constructor(&mut self, _: &Constructor) {}
423    }
424    program.visit_with(&mut Collector { f });
425}
426
427/// Whether `module.exports` is ever reassigned to a value that isn't safe.
428///
429/// A reassignment to an alias (`module.exports = require('./x')`,
430/// `module.exports = other`) would make later changes to properties on
431/// `module.exports` have side effects. A fresh object/array literal is safe,
432/// but not one capturing a value the module does not own
433/// (`module.exports = { g: globalThis }` is unsafe: a later
434/// `module.exports.g.x = 1` mutates the global).
435fn module_exports_is_tainted(program: &Program, unresolved_mark: Mark) -> bool {
436    let mut tainted = false;
437    for_each_top_level_expr(program, |expr| {
438        if let Expr::Assign(assign) = expr
439            && assign.op == AssignOp::Assign
440            && let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assign.left
441            && is_module_dot_exports(member, unresolved_mark)
442            && (!is_object_or_array_literal(&assign.right) || !is_fresh_value(&assign.right))
443        {
444            tainted = true;
445        }
446    });
447    tainted
448}
449
450/// Whether any value assigned to the module's CommonJS exports carries a getter
451/// or setter, or an `Object.defineProperty` on them installs one.
452///
453/// Attaching an accessor to the exports object makes later member access (read or
454/// write) potentially effectful — a subsequent `module.exports.foo = 1` could
455/// invoke a `set foo` — so once one is present, writes to the exports can no
456/// longer be treated as plain data assignments.
457fn module_exports_has_accessor(program: &Program, unresolved_mark: Mark) -> bool {
458    let mut found = false;
459    for_each_top_level_expr(program, |expr| match expr {
460        Expr::Assign(assign) => {
461            if assign.op == AssignOp::Assign
462                && let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assign.left
463                && is_cjs_export_member(member, unresolved_mark)
464                && contains_getters_or_setters(&assign.right)
465            {
466                found = true;
467            }
468        }
469        // look for `Object.defineProperty(exports, …)`
470        Expr::Call(call) => {
471            let is_accessor_key = |key: &PropName| {
472                // A `get`/`set` in the descriptor installs an accessor, and a spread or a
473                // computed key could carry one.
474                matches!(key, PropName::Computed(_))
475                    || prop_name_eq(key, "get")
476                    || prop_name_eq(key, "set")
477            };
478            if let Some((_, descriptor)) = as_exports_define_property(call, unresolved_mark)
479                && descriptor.props.iter().any(|prop| match prop {
480                    PropOrSpread::Prop(prop) => match &**prop {
481                        // The attached value carrying one matters too: a write
482                        // through it (`exports.foo.a = 1`) could invoke a setter.
483                        Prop::KeyValue(kv) => {
484                            is_accessor_key(&kv.key) || contains_getters_or_setters(&kv.value)
485                        }
486                        Prop::Method(method) => is_accessor_key(&method.key),
487                        Prop::Shorthand(ident) => matches!(ident.sym.as_ref(), "get" | "set"),
488                        _ => false,
489                    },
490                    PropOrSpread::Spread(_) => true,
491                })
492            {
493                found = true;
494            }
495        }
496        _ => {}
497    });
498    found
499}
500
501/// Analyzes a program to determine if it contains side effects at the top level.
502pub fn compute_module_evaluation_side_effects(
503    program: &Program,
504    comments: &dyn Comments,
505    unresolved_mark: Mark,
506) -> ModuleSideEffects {
507    let module_exports_tainted = module_exports_is_tainted(program, unresolved_mark);
508    let module_exports_has_accessor = module_exports_has_accessor(program, unresolved_mark);
509    let safe_assignment_constant_ids = collect_safe_assignment_constant_ids(program);
510    let mut visitor = SideEffectVisitor::new(
511        comments,
512        unresolved_mark,
513        module_exports_tainted,
514        module_exports_has_accessor,
515        safe_assignment_constant_ids,
516    );
517    program.visit_with(&mut visitor);
518    if visitor.has_side_effects {
519        ModuleSideEffects::SideEffectful
520    } else if visitor.has_imports {
521        ModuleSideEffects::ModuleEvaluationIsSideEffectFree
522    } else {
523        ModuleSideEffects::SideEffectFree
524    }
525}
526
527struct SideEffectVisitor<'a> {
528    comments: &'a dyn Comments,
529    unresolved_mark: Mark,
530    /// Whether `module.exports` was reassigned to a non-safe value, making member
531    /// writes to `module.exports.*` potentially observable.
532    module_exports_tainted: bool,
533    /// Whether a getter or setter is attached to the exports object, making any
534    /// write to the CommonJS exports potentially observable.
535    module_exports_has_accessor: bool,
536    /// local `const` bindings initialized with a fresh object/array literal.
537    /// Member mutations rooted at these are not module-evaluation side effects.
538    safe_assignment_constant_ids: HashSet<Id>,
539    has_side_effects: bool,
540    will_invoke_fn_exprs: bool,
541    has_imports: bool,
542}
543
544impl<'a> SideEffectVisitor<'a> {
545    fn new(
546        comments: &'a dyn Comments,
547        unresolved_mark: Mark,
548        module_exports_tainted: bool,
549        module_exports_has_accessor: bool,
550        safe_assignment_constant_ids: HashSet<Id>,
551    ) -> Self {
552        Self {
553            comments,
554            unresolved_mark,
555            module_exports_tainted,
556            module_exports_has_accessor,
557            safe_assignment_constant_ids,
558            has_side_effects: false,
559            will_invoke_fn_exprs: false,
560            has_imports: false,
561        }
562    }
563
564    /// Mark that we've found a side effect and stop further analysis.
565    fn mark_side_effect(&mut self) {
566        self.has_side_effects = true;
567    }
568
569    /// Temporarily set `will_invoke_fn_exprs` to the given value, execute the closure,
570    /// then restore the original value.
571    ///
572    /// This is useful when analyzing code that may invoke function expressions passed as
573    /// arguments (e.g., callbacks to pure functions like `array.map(fn)`).
574    fn with_will_invoke_fn_exprs<F>(&mut self, value: bool, f: F)
575    where
576        F: FnOnce(&mut Self),
577    {
578        let old_value = self.will_invoke_fn_exprs;
579        self.will_invoke_fn_exprs = value;
580        f(self);
581        self.will_invoke_fn_exprs = old_value;
582    }
583
584    /// Check if a span has a `/*#__PURE__*/` or `/*@__PURE__*/` annotation.
585    fn is_pure_annotated(&self, span: swc_core::common::Span) -> bool {
586        self.comments.has_flag(span.lo, "PURE")
587    }
588
589    /// Check if a callee expression is a known pure built-in function.
590    ///
591    /// This checks if the callee matches patterns like `Math.abs`, `Object.keys`, etc.
592    fn is_known_pure_builtin(&self, callee: &Callee) -> bool {
593        match callee {
594            Callee::Expr(expr) => self.is_known_pure_builtin_function(expr),
595            _ => false,
596        }
597    }
598    /// Returns true if this call is to `import()` or `require()`.
599    /// This is conservative since we don't resolve aliases and also because we don't support things
600    /// like `require.context` or `import.meta` apis
601    fn is_require_or_import(&self, callee: &Callee) -> bool {
602        match callee {
603            Callee::Expr(expr) => {
604                let expr = unparen(expr);
605                if let Expr::Ident(ident) = expr {
606                    is_global(ident, "require", self.unresolved_mark)
607                } else {
608                    false
609                }
610            }
611
612            Callee::Import(_) => true,
613            _ => false,
614        }
615    }
616
617    /// Whether writing this assignment target is unobservable during module
618    /// evaluation, so the write itself is not a side effect (the assigned value
619    /// and any computed key are still checked separately). Two pure cases:
620    /// - the module's own CommonJS exports (`exports.x`, `module.exports`, `module.exports.x`) —
621    ///   the CJS equivalent of an ESM `export`;
622    /// - a member mutation rooted at a `const` bound to an unaliased literal.
623    fn assign_target_is_pure(&self, target: &AssignTarget) -> bool {
624        match target {
625            AssignTarget::Simple(SimpleAssignTarget::Member(member)) => {
626                self.member_target_is_pure(member)
627            }
628            _ => false,
629        }
630    }
631
632    /// `a.b.c`-style target: pure if it writes the module's own CJS exports, or
633    /// is rooted at a `const` holding an unaliased object/array literal.
634    fn member_target_is_pure(&self, member: &MemberExpr) -> bool {
635        // If `module.exports` was reassigned to a non-safe value, writing its
636        // members may invoke a setter or mutate another module's object, so it
637        // is not safe even though it targets the CJS exports.
638        if self.module_exports_tainted && is_module_exports_chain(&member.obj, self.unresolved_mark)
639        {
640            return false;
641        }
642        // A write to the module's own CommonJS exports is the CJS form of an
643        // `export` — unless a getter/setter is attached to the exports object, in
644        // which case the write could invoke an accessor.
645        if is_cjs_export_member(member, self.unresolved_mark) {
646            return !self.module_exports_has_accessor;
647        }
648        // A member mutation rooted at a `const` bound to an unaliased object/array
649        // literal is also unobservable during evaluation.
650        let Some(root) = root_identifier(&member.obj) else {
651            return false;
652        };
653        self.safe_assignment_constant_ids.contains(&root.to_id())
654    }
655
656    /// Whether `call` is an `Object.defineProperty(exports, …)` touching only this
657    /// module's own exports — equivalent to `exports.<x> = …`.
658    fn define_property_target_is_pure(&self, call: &CallExpr) -> bool {
659        let Some((_, descriptor)) = as_exports_define_property(call, self.unresolved_mark) else {
660            return false;
661        };
662        let [target, ..] = &call.args[..] else {
663            return false;
664        };
665        if self.module_exports_tainted
666            && is_module_exports_chain(&target.expr, self.unresolved_mark)
667        {
668            return false;
669        }
670        // Bail on `{ get value() { … } }`
671        !descriptor.props.iter().any(|prop| {
672            matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::Getter(_) | Prop::Setter(_)))
673        })
674    }
675
676    /// Check if an expression is a known pure built-in function.
677    ///
678    /// This checks for:
679    /// - Member expressions like `Math.abs`, `Object.keys`, etc.
680    /// - Global function identifiers like `isNaN`, `parseInt`, etc.
681    /// - Literal receiver methods like `"hello".toLowerCase()`, `[1,2,3].map()`, etc.
682    ///
683    /// Only returns true if the base identifier is in the global scope (unresolved).
684    /// If it's shadowed by a local variable, we cannot assume it's the built-in.
685    fn is_known_pure_builtin_function(&self, expr: &Expr) -> bool {
686        match expr {
687            Expr::Member(member) => {
688                let receiver = unparen(&member.obj);
689                match (receiver, &member.prop) {
690                    // Handle global object methods like Math.abs, Object.keys, etc.
691                    (Expr::Ident(obj), MemberProp::Ident(prop)) => {
692                        // Only consider it pure if the base identifier is unresolved (global
693                        // scope). Check if the identifier's context matches
694                        // the unresolved mark.
695                        if obj.ctxt.outer() != self.unresolved_mark {
696                            // The identifier is in a local scope, might be shadowed
697                            return false;
698                        }
699
700                        // O(1) lookup: check if the object has the method in our known pure
701                        // functions
702                        KNOWN_PURE_FUNCTIONS
703                            .get(obj.sym.as_ref())
704                            .map(|methods| methods.contains(prop.sym.as_ref()))
705                            .unwrap_or(false)
706                    }
707                    // Handle literal receiver methods like "hello".toLowerCase(), [1,2,3].map(),
708                    // etc.
709                    (Expr::Lit(lit), MemberProp::Ident(prop)) => {
710                        let method_name = prop.sym.as_ref();
711                        match lit {
712                            Lit::Str(_) => {
713                                KNOWN_PURE_STRING_PROTOTYPE_METHODS.contains(method_name)
714                                    || KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(method_name)
715                            }
716                            Lit::Num(_) => {
717                                KNOWN_PURE_NUMBER_PROTOTYPE_METHODS.contains(method_name)
718                                    || KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(method_name)
719                            }
720                            Lit::Bool(_) => {
721                                KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(method_name)
722                            }
723                            Lit::Regex(_) => {
724                                KNOWN_PURE_REGEXP_PROTOTYPE_METHODS.contains(method_name)
725                                    || KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(method_name)
726                            }
727                            _ => false,
728                        }
729                    }
730                    // Handle array literal methods like [1,2,3].map()
731                    // Note: We don't check array elements here - that's handled in visit_expr
732                    (Expr::Array(_), MemberProp::Ident(prop)) => {
733                        let method_name = prop.sym.as_ref();
734                        KNOWN_PURE_ARRAY_PROTOTYPE_METHODS.contains(method_name)
735                            || KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(method_name)
736                    }
737                    (Expr::Object(_), MemberProp::Ident(prop)) => {
738                        KNOWN_PURE_OBJECT_PROTOTYPE_METHODS.contains(prop.sym.as_ref())
739                    }
740                    _ => false,
741                }
742            }
743            Expr::Ident(ident) => {
744                // Check for global pure functions like isNaN, parseInt, etc.
745                // Only consider it pure if the identifier is unresolved (global scope).
746                if ident.ctxt.outer() != self.unresolved_mark {
747                    return false;
748                }
749
750                // O(1) lookup in the global functions set
751                KNOWN_PURE_GLOBAL_FUNCTIONS.contains(ident.sym.as_ref())
752            }
753            _ => false,
754        }
755    }
756
757    /// Check if an expression is a known pure constructor.
758    ///
759    /// These are built-in constructors that create new objects without side effects.
760    /// Only returns true if the identifier is in the global scope (unresolved).
761    /// If it's shadowed by a local variable, we cannot assume it's the built-in constructor.
762    fn is_known_pure_constructor(&self, expr: &Expr) -> bool {
763        match expr {
764            Expr::Ident(ident) => {
765                // Only consider it pure if the identifier is unresolved (global scope).
766                // Check if the identifier's context matches the unresolved mark.
767                if ident.ctxt.outer() != self.unresolved_mark {
768                    return false;
769                }
770
771                // O(1) lookup in the constructors set
772                KNOWN_PURE_CONSTRUCTORS.contains(ident.sym.as_ref())
773            }
774            _ => false,
775        }
776    }
777}
778
779impl<'a> Visit for SideEffectVisitor<'a> {
780    noop_visit_type!();
781    // If we've already found side effects, skip further visitation
782    fn visit_program(&mut self, program: &Program) {
783        check_side_effects!(self);
784        program.visit_children_with(self);
785    }
786
787    fn visit_module(&mut self, module: &Module) {
788        check_side_effects!(self);
789
790        // Only check top-level module items
791        for item in &module.body {
792            check_side_effects!(self);
793            item.visit_with(self);
794        }
795    }
796
797    fn visit_script(&mut self, script: &Script) {
798        check_side_effects!(self);
799
800        // Only check top-level statements
801        for stmt in &script.body {
802            check_side_effects!(self);
803            stmt.visit_with(self);
804        }
805    }
806
807    // Module declarations (imports/exports) need special handling
808    fn visit_module_decl(&mut self, decl: &ModuleDecl) {
809        check_side_effects!(self);
810
811        match decl {
812            // Import statements may have side effects, which could require full graph analysis
813            // Record that to decide if we can upgrade ModuleEvaluationIsSideEffectFree to
814            // SideEffectFree
815            ModuleDecl::Import(_) => {
816                self.has_imports = true;
817            }
818
819            // Export declarations need to check their contents
820            ModuleDecl::ExportDecl(export_decl) => {
821                // Check the declaration being exported
822                match &export_decl.decl {
823                    Decl::Fn(_) => {
824                        // function declarations are pure
825                    }
826                    Decl::Class(class_decl) => {
827                        // Class declarations can have side effects in static blocks, extends or
828                        // static property initializers.
829                        class_decl.visit_with(self);
830                    }
831                    Decl::Var(var_decl) => {
832                        // Variable declarations need their initializers checked
833                        var_decl.visit_with(self);
834                    }
835                    _ => {
836                        // Other declarations should be checked
837                        export_decl.decl.visit_with(self);
838                    }
839                }
840            }
841
842            ModuleDecl::ExportDefaultDecl(export_default_decl) => {
843                // Check the default export
844                match &export_default_decl.decl {
845                    DefaultDecl::Class(cls) => {
846                        // Class expressions can have side effects in extends clause and static
847                        // members
848                        cls.visit_with(self);
849                    }
850                    DefaultDecl::Fn(_) => {
851                        // function declarations are pure
852                    }
853                    DefaultDecl::TsInterfaceDecl(_) => {
854                        // TypeScript interface declarations are pure
855                    }
856                }
857            }
858
859            ModuleDecl::ExportDefaultExpr(export_default_expr) => {
860                // Check the expression being exported
861                export_default_expr.expr.visit_with(self);
862            }
863
864            // Re-exports have no side effects
865            ModuleDecl::ExportNamed(e) => {
866                if e.src.is_some() {
867                    // reexports are also imports
868                    self.has_imports = true;
869                }
870            }
871            ModuleDecl::ExportAll(_) => {
872                // reexports are also imports
873                self.has_imports = true;
874            }
875
876            // TypeScript-specific exports
877            ModuleDecl::TsExportAssignment(_) | ModuleDecl::TsNamespaceExport(_) => {}
878            ModuleDecl::TsImportEquals(e) => {
879                // The RHS of a ts import equals expression is typically an identifier but it might
880                // also be a require!
881                match &e.module_ref {
882                    TsModuleRef::TsEntityName(_) => {}
883                    TsModuleRef::TsExternalModuleRef(_) => {
884                        // This is a `import x = require('y')` call
885                        self.has_imports = true
886                    }
887                }
888            }
889        }
890    }
891
892    // Statement-level detection
893    fn visit_stmt(&mut self, stmt: &Stmt) {
894        check_side_effects!(self);
895
896        match stmt {
897            // Expression statements need checking
898            Stmt::Expr(expr_stmt) => {
899                expr_stmt.visit_with(self);
900            }
901            // Variable declarations need checking (initializers might have side effects)
902            Stmt::Decl(Decl::Var(var_decl)) => {
903                var_decl.visit_with(self);
904            }
905            // Function declarations are side-effect free
906            Stmt::Decl(Decl::Fn(_)) => {
907                // Function declarations don't execute, so no side effects
908            }
909            // Class declarations can have side effects in extends clause and static members
910            Stmt::Decl(Decl::Class(class_decl)) => {
911                class_decl.visit_with(self);
912            }
913            // Other declarations
914            Stmt::Decl(decl) => {
915                decl.visit_with(self);
916            }
917            // For other statement types, be conservative
918            _ => {
919                // Most other statement types (if, for, while, etc.) at top level
920                // would be unusual and potentially have side effects
921                self.mark_side_effect();
922            }
923        }
924    }
925
926    fn visit_var_declarator(&mut self, var_decl: &VarDeclarator) {
927        check_side_effects!(self);
928
929        // Check the pattern (for default values in destructuring)
930        var_decl.name.visit_with(self);
931
932        // Check the initializer
933        if let Some(init) = &var_decl.init {
934            init.visit_with(self);
935        }
936    }
937
938    // Expression-level detection
939    fn visit_expr(&mut self, expr: &Expr) {
940        check_side_effects!(self);
941
942        match expr {
943            // Pure expressions
944            Expr::Lit(_) => {
945                // Literals are always pure
946            }
947            Expr::Ident(_) => {
948                // Reading identifiers is pure
949            }
950            Expr::Arrow(_) | Expr::Fn(_) => {
951                // Function expressions are pure (don't execute until called)
952                if self.will_invoke_fn_exprs {
953                    // assume that any nested function expressions will not be invoked.
954                    self.with_will_invoke_fn_exprs(false, |this| {
955                        expr.visit_children_with(this);
956                    });
957                }
958            }
959            Expr::Class(class_expr) => {
960                // Class expressions can have side effects in extends clause and static members
961                class_expr.class.visit_with(self);
962            }
963            Expr::Array(arr) => {
964                // Arrays are pure if their elements are pure
965                for elem in arr.elems.iter().flatten() {
966                    elem.visit_with(self);
967                }
968            }
969            Expr::Object(obj) => {
970                // Objects are pure if their property names and initializers
971                for prop in &obj.props {
972                    prop.visit_with(self);
973                }
974            }
975            Expr::Unary(unary) => {
976                // Most unary operations are pure, but delete is not
977                if unary.op == UnaryOp::Delete {
978                    // TODO: allow deletes to module level variables or properties defined on module
979                    // level variables
980                    self.mark_side_effect();
981                } else {
982                    unary.arg.visit_with(self);
983                }
984            }
985            Expr::Bin(bin) => {
986                // Binary operations are pure if operands are pure
987                bin.left.visit_with(self);
988                bin.right.visit_with(self);
989            }
990            Expr::Cond(cond) => {
991                // Conditional is pure if all parts are pure
992                cond.test.visit_with(self);
993                cond.cons.visit_with(self);
994                cond.alt.visit_with(self);
995            }
996            Expr::Member(member) => {
997                // Member access is pure - just reading a property doesn't cause side effects.
998                // While getters *could* have side effects, in practice:
999                // 1. Most code doesn't use getters with side effects (rare pattern)
1000                // 2. Webpack and rolldown treat member access as pure
1001                // 3. Being too conservative here would mark too much code as impure
1002                //
1003                // We check the object and property for side effects (e.g., computed properties)
1004                member.obj.visit_with(self);
1005                member.prop.visit_with(self);
1006            }
1007            Expr::Paren(paren) => {
1008                // Parenthesized expressions inherit purity from inner expr
1009                paren.expr.visit_with(self);
1010            }
1011            Expr::Tpl(tpl) => {
1012                // Template literals are pure if expressions are pure
1013                for expr in &tpl.exprs {
1014                    expr.visit_with(self);
1015                }
1016            }
1017
1018            // Impure expressions (conservative)
1019            Expr::Call(call) => {
1020                // Check for /*#__PURE__*/ annotation or for a well known function
1021                let is_pure_annotated = self.is_pure_annotated(call.span);
1022                if is_pure_annotated || self.is_known_pure_builtin(&call.callee) {
1023                    // For known pure builtins, we need to check both:
1024                    // 1. The receiver (e.g., the array in [foo(), 2, 3].map(...))
1025                    // 2. The arguments
1026
1027                    // Check the receiver
1028                    call.callee.visit_with(self);
1029
1030                    // Check all arguments
1031                    // Known builtins may invoke callbacks, but a PURE annotation covers
1032                    // those effects too. Argument evaluation must still be checked.
1033                    self.with_will_invoke_fn_exprs(!is_pure_annotated, |this| {
1034                        call.args.visit_children_with(this);
1035                    });
1036                } else if self.is_require_or_import(&call.callee) {
1037                    self.has_imports = true;
1038                    // It would be weird to have a side effect in a require(...) statement, but not
1039                    // impossible.
1040                    call.args.visit_children_with(self);
1041                } else if self.define_property_target_is_pure(call) {
1042                    call.args.visit_children_with(self);
1043                } else {
1044                    // Unmarked calls are considered to have side effects
1045                    self.mark_side_effect();
1046                }
1047            }
1048            Expr::New(new) => {
1049                // Check for /*#__PURE__*/ annotation or known pure constructor
1050                let is_pure_annotated = self.is_pure_annotated(new.span);
1051                if is_pure_annotated || self.is_known_pure_constructor(&new.callee) {
1052                    // The annotation only covers invocation, not callee or argument evaluation.
1053                    new.callee.visit_with(self);
1054                    self.with_will_invoke_fn_exprs(!is_pure_annotated, |this| {
1055                        new.args.visit_children_with(this);
1056                    });
1057                } else {
1058                    // Unknown constructor calls are considered to have side effects
1059                    self.mark_side_effect();
1060                }
1061            }
1062            Expr::Assign(assign) => {
1063                // Assigning to the module's own CommonJS exports (`exports.x`,
1064                // `module.exports`, `module.exports.x`) is the CJS equivalent of an
1065                // ESM `export` declaration.
1066                //
1067                // Accessor handling lives in the collection passes: a binding (or
1068                // the exports object) that ever has a getter/setter attached to it
1069                // is excluded up front, so `assign_target_is_pure` already returns
1070                // false for member writes that could invoke one.
1071                if assign.op == AssignOp::Assign && self.assign_target_is_pure(&assign.left) {
1072                    // Still check the assigned value, and the target's computed
1073                    // property keys (e.g. `exports[sideEffect()] = …`).
1074                    assign.left.visit_with(self);
1075                    assign.right.visit_with(self);
1076                } else {
1077                    self.mark_side_effect();
1078                }
1079            }
1080            Expr::Update(_) => {
1081                // Updates (++, --) have side effects
1082                // TODO: allow updates to module level variables
1083                self.mark_side_effect();
1084            }
1085            Expr::Await(e) => {
1086                e.arg.visit_with(self);
1087            }
1088            Expr::Yield(e) => {
1089                e.arg.visit_with(self);
1090            }
1091            Expr::TaggedTpl(tagged_tpl)
1092                // Tagged template literals are function calls
1093                // But some are known to be pure, like String.raw
1094                if self.is_known_pure_builtin_function(&tagged_tpl.tag) => {
1095                    for arg in &tagged_tpl.tpl.exprs {
1096                        arg.visit_with(self);
1097                    }
1098                }
1099            Expr::OptChain(opt_chain) => {
1100                // Optional chaining can be pure if it's just member access
1101                // But if it's an optional call, it has side effects
1102                opt_chain.base.visit_with(self);
1103            }
1104            Expr::Seq(seq) => {
1105                // Sequence expressions - check each expression
1106                seq.exprs.visit_children_with(self);
1107            }
1108            Expr::SuperProp(super_prop) => {
1109                // Super property access is pure (reading from parent class)
1110                // Check if the property expression has side effects
1111                super_prop.prop.visit_with(self);
1112            }
1113            Expr::MetaProp(_) => {
1114                // Meta properties like import.meta and new.target are pure
1115                // They just read metadata, don't cause side effects
1116            }
1117            Expr::JSXMember(_) | Expr::JSXNamespacedName(_) | Expr::JSXEmpty(_) => {
1118                // JSX member expressions and names are pure (they're just identifiers)
1119            }
1120            Expr::JSXElement(_) | Expr::JSXFragment(_) => {
1121                // JSX elements compile to function calls (React.createElement, etc.)
1122                // These are side effect free but we don't technically know at this point that it is
1123                // react (could be solid or qwik or millionjs).  In any case it doesn't matter too
1124                // much since it is weird to construct jsx at the module scope.
1125                self.mark_side_effect();
1126            }
1127            Expr::PrivateName(_) => {
1128                // Private names are pure (just identifiers)
1129            }
1130
1131            // Be conservative for other expression types and just assume they are effectful
1132            _ => {
1133                self.mark_side_effect();
1134            }
1135        }
1136    }
1137
1138    fn visit_opt_chain_base(&mut self, base: &OptChainBase) {
1139        check_side_effects!(self);
1140
1141        match base {
1142            OptChainBase::Member(member) => {
1143                member.visit_with(self);
1144            }
1145            OptChainBase::Call(_opt_call) => {
1146                // Optional calls are still calls, so impure
1147                // We could maybe support some of these `(foo_enabled? undefined :
1148                // [])?.map(...)` but this seems pretty theoretical
1149                self.mark_side_effect();
1150            }
1151        }
1152    }
1153
1154    fn visit_prop_or_spread(&mut self, prop: &PropOrSpread) {
1155        check_side_effects!(self);
1156
1157        match prop {
1158            PropOrSpread::Spread(spread) => {
1159                spread.expr.visit_with(self);
1160            }
1161            PropOrSpread::Prop(prop) => {
1162                prop.visit_with(self);
1163            }
1164        }
1165    }
1166
1167    fn visit_prop(&mut self, prop: &Prop) {
1168        check_side_effects!(self);
1169
1170        match prop {
1171            Prop::KeyValue(kv) => {
1172                kv.key.visit_with(self);
1173                kv.value.visit_with(self);
1174            }
1175            Prop::Getter(getter) => {
1176                getter.key.visit_with(self);
1177                // Body is not executed at definition time
1178            }
1179            Prop::Setter(setter) => {
1180                setter.key.visit_with(self);
1181                // Body is not executed at definition time
1182            }
1183            Prop::Method(method) => {
1184                method.key.visit_with(self);
1185                // Body is not executed at definition time
1186            }
1187            Prop::Shorthand(_) => {
1188                // Shorthand properties are pure
1189            }
1190            Prop::Assign(_) => {
1191                // Assignment properties (used in object rest/spread patterns)
1192                // are side-effect free at definition
1193            }
1194        }
1195    }
1196
1197    fn visit_prop_name(&mut self, prop_name: &PropName) {
1198        check_side_effects!(self);
1199
1200        match prop_name {
1201            PropName::Computed(computed) => {
1202                // Computed property names need evaluation
1203                computed.expr.visit_with(self);
1204            }
1205            _ => {
1206                // Other property names are pure
1207            }
1208        }
1209    }
1210
1211    fn visit_class(&mut self, class: &Class) {
1212        check_side_effects!(self);
1213
1214        // Check decorators - they execute at definition time
1215        for decorator in &class.decorators {
1216            decorator.visit_with(self);
1217        }
1218
1219        // Check the extends clause - this is evaluated at definition time
1220        if let Some(super_class) = &class.super_class {
1221            super_class.visit_with(self);
1222        }
1223
1224        // Check class body for static members
1225        for member in &class.body {
1226            member.visit_with(self);
1227        }
1228    }
1229
1230    fn visit_class_member(&mut self, member: &ClassMember) {
1231        check_side_effects!(self);
1232
1233        match member {
1234            // Static blocks execute at class definition time
1235            ClassMember::StaticBlock(block) => {
1236                // Static blocks may have side effects because they execute immediately
1237                // Check the statements in the block
1238                for stmt in &block.body.stmts {
1239                    stmt.visit_with(self);
1240                }
1241            }
1242            // Check static properties - they execute at definition time
1243            ClassMember::ClassProp(class_prop) if class_prop.is_static => {
1244                // Check decorators - they execute at definition time
1245                for decorator in &class_prop.decorators {
1246                    decorator.visit_with(self);
1247                }
1248                // Check the property key (for computed properties)
1249                class_prop.key.visit_with(self);
1250                // Check the initializer - static property initializers execute at definition time
1251                if let Some(value) = &class_prop.value {
1252                    value.visit_with(self);
1253                }
1254            }
1255            // Check computed property keys for all members
1256            ClassMember::Method(method) => {
1257                // Check decorators - they execute at definition time
1258                for decorator in &method.function.decorators {
1259                    decorator.visit_with(self);
1260                }
1261                method.key.visit_with(self);
1262                // Method bodies don't execute at definition time
1263            }
1264            ClassMember::Constructor(constructor) => {
1265                constructor.key.visit_with(self);
1266                // Constructor body doesn't execute at definition time
1267            }
1268            ClassMember::PrivateMethod(private_method) => {
1269                // Check decorators - they execute at definition time
1270                for decorator in &private_method.function.decorators {
1271                    decorator.visit_with(self);
1272                }
1273                private_method.key.visit_with(self);
1274                // Method bodies don't execute at definition time
1275            }
1276            ClassMember::ClassProp(class_prop) => {
1277                // Check decorators - they execute at definition time
1278                for decorator in &class_prop.decorators {
1279                    decorator.visit_with(self);
1280                }
1281                // For non-static properties, only check the key
1282                class_prop.key.visit_with(self);
1283                // Instance property initializers don't execute at definition time
1284            }
1285            ClassMember::PrivateProp(private_prop) => {
1286                // Check decorators - they execute at definition time
1287                for decorator in &private_prop.decorators {
1288                    decorator.visit_with(self);
1289                }
1290                private_prop.key.visit_with(self);
1291                // Instance property initializers don't execute at definition time
1292            }
1293            ClassMember::AutoAccessor(auto_accessor) if auto_accessor.is_static => {
1294                // Check decorators - they execute at definition time
1295                for decorator in &auto_accessor.decorators {
1296                    decorator.visit_with(self);
1297                }
1298                // Static auto accessors execute at definition time
1299                auto_accessor.key.visit_with(self);
1300                if let Some(value) = &auto_accessor.value {
1301                    value.visit_with(self);
1302                }
1303            }
1304            ClassMember::AutoAccessor(auto_accessor) => {
1305                // Check decorators - they execute at definition time
1306                for decorator in &auto_accessor.decorators {
1307                    decorator.visit_with(self);
1308                }
1309                // Non-static auto accessors only check the key
1310                auto_accessor.key.visit_with(self);
1311            }
1312            ClassMember::Empty(_) => {
1313                // Empty members are pure
1314            }
1315            ClassMember::TsIndexSignature(_) => {
1316                // TypeScript index signatures are pure
1317            }
1318        }
1319    }
1320
1321    fn visit_decorator(&mut self, _decorator: &Decorator) {
1322        if self.has_side_effects {
1323            return;
1324        }
1325
1326        // Decorators always have side effects because they are function calls
1327        // that execute at class/member definition time, even if they're just
1328        // identifier references (e.g., @decorator is equivalent to calling decorator())
1329        self.mark_side_effect();
1330    }
1331
1332    fn visit_pat(&mut self, pat: &Pat) {
1333        check_side_effects!(self);
1334
1335        match pat {
1336            // Object patterns with default values need checking
1337            Pat::Object(object_pat) => {
1338                for prop in &object_pat.props {
1339                    match prop {
1340                        ObjectPatProp::KeyValue(kv) => {
1341                            // Check the key (for computed properties)
1342                            kv.key.visit_with(self);
1343                            // Recursively check the value pattern
1344                            kv.value.visit_with(self);
1345                        }
1346                        ObjectPatProp::Assign(assign) => {
1347                            // Check the default value if present
1348                            if let Some(value) = &assign.value {
1349                                value.visit_with(self);
1350                            }
1351                        }
1352                        ObjectPatProp::Rest(rest) => {
1353                            // Rest patterns are pure, but check the nested pattern
1354                            rest.arg.visit_with(self);
1355                        }
1356                    }
1357                }
1358            }
1359            // Array patterns with default values need checking
1360            Pat::Array(array_pat) => {
1361                for elem in array_pat.elems.iter().flatten() {
1362                    elem.visit_with(self);
1363                }
1364            }
1365            // Assignment patterns (destructuring with defaults) need checking
1366            Pat::Assign(assign_pat) => {
1367                // Check the default value - this is evaluated if the value is undefined
1368                assign_pat.right.visit_with(self);
1369                // Also check the left side pattern
1370                assign_pat.left.visit_with(self);
1371            }
1372            // Other patterns are pure
1373            _ => {}
1374        }
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use swc_core::{
1381        common::{FileName, GLOBALS, Mark, SourceMap, comments::SingleThreadedComments, sync::Lrc},
1382        ecma::{
1383            ast::EsVersion,
1384            parser::{EsSyntax, Syntax, parse_file_as_program},
1385            transforms::base::resolver,
1386            visit::VisitMutWith,
1387        },
1388    };
1389
1390    use super::*;
1391
1392    /// Helper function to parse JavaScript code from a string and run the resolver
1393    fn parse_and_check_for_side_effects(code: &str, expected: ModuleSideEffects) {
1394        GLOBALS.set(&Default::default(), || {
1395            let cm = Lrc::new(SourceMap::default());
1396            let fm = cm.new_source_file(Lrc::new(FileName::Anon), code.to_string());
1397
1398            let comments = SingleThreadedComments::default();
1399            let mut errors = vec![];
1400
1401            let mut program = parse_file_as_program(
1402                &fm,
1403                Syntax::Es(EsSyntax {
1404                    jsx: true,
1405                    decorators: true,
1406                    ..Default::default()
1407                }),
1408                EsVersion::latest(),
1409                Some(&comments),
1410                &mut errors,
1411            )
1412            .expect("Failed to parse");
1413
1414            // Run the resolver to mark unresolved identifiers
1415            let unresolved_mark = Mark::new();
1416            let top_level_mark = Mark::new();
1417            program.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
1418
1419            let actual =
1420                compute_module_evaluation_side_effects(&program, &comments, unresolved_mark);
1421
1422            let msg = match expected {
1423                ModuleSideEffects::ModuleEvaluationIsSideEffectFree => {
1424                    "Expected code to have no local side effects"
1425                }
1426                ModuleSideEffects::SideEffectFree => "Expected code to be side effect free",
1427                ModuleSideEffects::SideEffectful => "Expected code to have side effects",
1428            };
1429            assert_eq!(actual, expected, "{}:\n{}", msg, code);
1430        })
1431    }
1432
1433    /// Generate a test that asserts the given code has the expected side effect status
1434    macro_rules! assert_side_effects {
1435        ($name:ident, $code:expr, $expected:expr) => {
1436            #[test]
1437            fn $name() {
1438                parse_and_check_for_side_effects($code, $expected);
1439            }
1440        };
1441    }
1442
1443    macro_rules! side_effects {
1444        ($name:ident, $code:expr) => {
1445            assert_side_effects!($name, $code, ModuleSideEffects::SideEffectful);
1446        };
1447    }
1448
1449    macro_rules! no_side_effects {
1450        ($name:ident, $code:expr) => {
1451            assert_side_effects!($name, $code, ModuleSideEffects::SideEffectFree);
1452        };
1453    }
1454    macro_rules! module_evaluation_is_side_effect_free {
1455        ($name:ident, $code:expr) => {
1456            assert_side_effects!(
1457                $name,
1458                $code,
1459                ModuleSideEffects::ModuleEvaluationIsSideEffectFree
1460            );
1461        };
1462    }
1463
1464    mod constructor_callee_tests {
1465        use super::*;
1466
1467        side_effects!(test_call_callee, "/*#__PURE__*/ new (getConstructor())();");
1468        side_effects!(
1469            test_nested_call_callee,
1470            "/*#__PURE__*/ new (getContainer().Constructor)();"
1471        );
1472        side_effects!(
1473            test_computed_callee,
1474            "/*#__PURE__*/ new (constructors[getKey()])();"
1475        );
1476        side_effects!(
1477            test_conditional_callee,
1478            "/*#__PURE__*/ new (flag ? getConstructor() : Constructor)();"
1479        );
1480        side_effects!(
1481            test_callee_and_arguments,
1482            "/*#__PURE__*/ new (getConstructor())(effect());"
1483        );
1484        side_effects!(test_arguments, "/*#__PURE__*/ new Constructor(effect());");
1485        no_side_effects!(test_identifier_callee, "/*#__PURE__*/ new Constructor();");
1486        no_side_effects!(
1487            test_member_callee,
1488            "/*#__PURE__*/ new constructors.Constructor();"
1489        );
1490        no_side_effects!(
1491            test_computed_pure_callee,
1492            "/*#__PURE__*/ new constructors[key]();"
1493        );
1494        no_side_effects!(
1495            test_uninvoked_constructor_body,
1496            "/*#__PURE__*/ new (function () { effect(); })();"
1497        );
1498        no_side_effects!(test_known_constructor, "new Map();");
1499        no_side_effects!(
1500            test_pure_callee_call,
1501            "/*#__PURE__*/ new (/*#__PURE__*/ getConstructor())();"
1502        );
1503    }
1504
1505    mod basic_tests {
1506        use super::*;
1507
1508        no_side_effects!(test_empty_program, "");
1509
1510        no_side_effects!(test_simple_const_declaration, "const x = 5;");
1511
1512        no_side_effects!(test_simple_let_declaration, "let y = 'string';");
1513
1514        no_side_effects!(test_array_literal, "const arr = [1, 2, 3];");
1515
1516        no_side_effects!(test_object_literal, "const obj = { a: 1, b: 2 };");
1517
1518        no_side_effects!(test_function_declaration, "function foo() { return 1; }");
1519
1520        no_side_effects!(
1521            test_function_expression,
1522            "const foo = function() { return 1; };"
1523        );
1524
1525        no_side_effects!(test_arrow_function, "const foo = () => 1;");
1526    }
1527
1528    mod side_effects_tests {
1529        use super::*;
1530
1531        side_effects!(test_console_log, "console.log('hello');");
1532
1533        side_effects!(test_function_call, "foo();");
1534
1535        side_effects!(test_method_call, "obj.method();");
1536
1537        side_effects!(test_assignment, "x = 5;");
1538
1539        side_effects!(test_member_assignment, "obj.prop = 5;");
1540
1541        side_effects!(test_constructor_call, "new SideEffect();");
1542
1543        side_effects!(test_update_expression, "x++;");
1544    }
1545
1546    mod pure_expressions_tests {
1547        use super::*;
1548
1549        no_side_effects!(test_binary_expression, "const x = 1 + 2;");
1550
1551        no_side_effects!(test_unary_expression, "const x = -5;");
1552
1553        no_side_effects!(test_conditional_expression, "const x = true ? 1 : 2;");
1554
1555        no_side_effects!(test_template_literal, "const x = `hello ${world}`;");
1556
1557        no_side_effects!(test_nested_object, "const obj = { a: { b: { c: 1 } } };");
1558
1559        no_side_effects!(test_nested_array, "const arr = [[1, 2], [3, 4]];");
1560    }
1561
1562    mod import_export_tests {
1563        use super::*;
1564
1565        module_evaluation_is_side_effect_free!(test_import_statement, "import x from 'y';");
1566        module_evaluation_is_side_effect_free!(test_require_statement, "const x = require('y');");
1567
1568        no_side_effects!(test_export_statement, "export default 5;");
1569
1570        no_side_effects!(test_export_const, "export const x = 5;");
1571
1572        side_effects!(
1573            test_export_const_with_side_effect,
1574            "export const x = foo();"
1575        );
1576    }
1577
1578    mod mixed_cases_tests {
1579        use super::*;
1580
1581        side_effects!(test_call_in_initializer, "const x = foo();");
1582
1583        side_effects!(test_call_in_array, "const arr = [1, foo(), 3];");
1584
1585        side_effects!(test_call_in_object, "const obj = { a: foo() };");
1586
1587        no_side_effects!(
1588            test_multiple_declarations_pure,
1589            "const x = 1;\nconst y = 2;\nconst z = 3;"
1590        );
1591
1592        side_effects!(
1593            test_multiple_declarations_with_side_effect,
1594            "const x = 1;\nfoo();\nconst z = 3;"
1595        );
1596
1597        no_side_effects!(test_class_declaration, "class Foo {}");
1598
1599        no_side_effects!(
1600            test_class_with_methods,
1601            "class Foo { method() { return 1; } }"
1602        );
1603    }
1604
1605    mod pure_annotations_tests {
1606        use super::*;
1607
1608        no_side_effects!(test_pure_annotation_function_call, "/*#__PURE__*/ foo();");
1609
1610        no_side_effects!(test_pure_annotation_with_at, "/*@__PURE__*/ foo();");
1611
1612        no_side_effects!(test_pure_annotation_constructor, "/*#__PURE__*/ new Foo();");
1613
1614        no_side_effects!(
1615            test_pure_annotation_in_variable,
1616            "const x = /*#__PURE__*/ foo();"
1617        );
1618
1619        no_side_effects!(
1620            test_pure_annotation_with_pure_args,
1621            "/*#__PURE__*/ foo(1, 2, 3, function(){ console.log(1); });"
1622        );
1623
1624        no_side_effects!(
1625            test_pure_annotation_with_arrow_callback,
1626            "/*#__PURE__*/ foo(() => console.log(1));"
1627        );
1628
1629        no_side_effects!(
1630            test_pure_annotation_builtin_with_callback,
1631            "/*#__PURE__*/ [1].map(() => console.log(1));"
1632        );
1633
1634        no_side_effects!(
1635            test_pure_annotation_constructor_with_callback,
1636            "/*#__PURE__*/ new Promise(() => console.log(1));"
1637        );
1638
1639        // Even with PURE annotation, impure arguments make it impure
1640        side_effects!(
1641            test_pure_annotation_with_impure_args,
1642            "/*#__PURE__*/ foo(bar());"
1643        );
1644
1645        // Without annotation, calls are impure
1646        side_effects!(test_without_pure_annotation, "foo();");
1647
1648        no_side_effects!(
1649            test_pure_nested_in_object,
1650            "const obj = { x: /*#__PURE__*/ foo() };"
1651        );
1652
1653        no_side_effects!(test_pure_in_array, "const arr = [/*#__PURE__*/ foo()];");
1654
1655        no_side_effects!(
1656            test_multiple_pure_calls,
1657            "const x = /*#__PURE__*/ foo();\nconst y = /*#__PURE__*/ bar();"
1658        );
1659
1660        side_effects!(
1661            test_mixed_pure_and_impure,
1662            "const x = /*#__PURE__*/ foo();\nbar();\nconst z = /*#__PURE__*/ baz();"
1663        );
1664    }
1665
1666    mod known_pure_builtins_tests {
1667        use super::*;
1668
1669        no_side_effects!(test_math_abs, "const x = Math.abs(-5);");
1670
1671        no_side_effects!(test_math_floor, "const x = Math.floor(3.14);");
1672
1673        no_side_effects!(test_math_max, "const x = Math.max(1, 2, 3);");
1674
1675        no_side_effects!(test_object_keys, "const keys = Object.keys(obj);");
1676
1677        no_side_effects!(test_object_values, "const values = Object.values(obj);");
1678
1679        no_side_effects!(test_object_entries, "const entries = Object.entries(obj);");
1680
1681        no_side_effects!(test_array_is_array, "const result = Array.isArray([]);");
1682
1683        no_side_effects!(
1684            test_string_from_char_code,
1685            "const char = String.fromCharCode(65);"
1686        );
1687
1688        no_side_effects!(test_number_is_nan, "const result = Number.isNaN(x);");
1689
1690        no_side_effects!(
1691            test_multiple_math_calls,
1692            "const x = Math.abs(-5);\nconst y = Math.floor(3.14);\nconst z = Math.max(x, y);"
1693        );
1694
1695        // Even pure builtins become impure if arguments are impure
1696        side_effects!(
1697            test_pure_builtin_with_impure_arg,
1698            "const x = Math.abs(foo());"
1699        );
1700
1701        no_side_effects!(
1702            test_pure_builtin_in_expression,
1703            "const x = Math.abs(-5) + Math.floor(3.14);"
1704        );
1705
1706        side_effects!(
1707            test_mixed_builtin_and_impure,
1708            "const x = Math.abs(-5);\nfoo();\nconst z = Object.keys({});"
1709        );
1710
1711        // Accessing unknown Math properties is not in our list
1712        side_effects!(test_unknown_math_property, "const x = Math.random();");
1713
1714        // Object.assign is NOT pure (it mutates)
1715        side_effects!(test_object_assign, "Object.assign(target, source);");
1716
1717        no_side_effects!(test_array_from, "const arr = Array.from(iterable);");
1718
1719        no_side_effects!(test_global_is_nan, "const result = isNaN(value);");
1720
1721        no_side_effects!(test_global_is_finite, "const result = isFinite(value);");
1722
1723        no_side_effects!(test_global_parse_int, "const num = parseInt('42', 10);");
1724
1725        no_side_effects!(test_global_parse_float, "const num = parseFloat('3.14');");
1726
1727        no_side_effects!(
1728            test_global_decode_uri,
1729            "const decoded = decodeURI(encoded);"
1730        );
1731
1732        no_side_effects!(
1733            test_global_decode_uri_component,
1734            "const decoded = decodeURIComponent(encoded);"
1735        );
1736
1737        // String() as a function (not constructor) is pure
1738        no_side_effects!(
1739            test_global_string_constructor_as_function,
1740            "const str = String(123);"
1741        );
1742
1743        // Number() as a function (not constructor) is pure
1744        no_side_effects!(
1745            test_global_number_constructor_as_function,
1746            "const num = Number('123');"
1747        );
1748
1749        // Boolean() as a function (not constructor) is pure
1750        no_side_effects!(
1751            test_global_boolean_constructor_as_function,
1752            "const bool = Boolean(value);"
1753        );
1754
1755        // Symbol() as a function is pure
1756        no_side_effects!(
1757            test_global_symbol_constructor_as_function,
1758            "const sym = Symbol('description');"
1759        );
1760
1761        // Symbol.for() is pure
1762        no_side_effects!(test_symbol_for, "const sym = Symbol.for('description');");
1763
1764        // Symbol.keyFor() is pure
1765        no_side_effects!(
1766            test_symbol_key_for,
1767            "const description = Symbol.keyFor(sym);"
1768        );
1769
1770        // Global pure function with impure argument is impure
1771        side_effects!(
1772            test_global_pure_with_impure_arg,
1773            "const result = isNaN(foo());"
1774        );
1775
1776        // isNaN shadowed at top level
1777        side_effects!(
1778            test_shadowed_global_is_nan,
1779            r#"
1780            const isNaN = () => sideEffect();
1781            const result = isNaN(value);
1782            "#
1783        );
1784    }
1785
1786    mod edge_cases_tests {
1787        use super::*;
1788
1789        no_side_effects!(test_computed_property, "const obj = { [key]: value };");
1790
1791        side_effects!(
1792            test_computed_property_with_call,
1793            "const obj = { [foo()]: value };"
1794        );
1795
1796        no_side_effects!(test_spread_in_array, "const arr = [...other];");
1797
1798        no_side_effects!(test_spread_in_object, "const obj = { ...other };");
1799
1800        no_side_effects!(test_destructuring_assignment, "const { a, b } = obj;");
1801
1802        no_side_effects!(test_array_destructuring, "const [a, b] = arr;");
1803
1804        no_side_effects!(test_nested_ternary, "const x = a ? (b ? 1 : 2) : 3;");
1805
1806        no_side_effects!(test_logical_and, "const x = a && b;");
1807
1808        no_side_effects!(test_logical_or, "const x = a || b;");
1809
1810        no_side_effects!(test_nullish_coalescing, "const x = a ?? b;");
1811
1812        no_side_effects!(test_typeof_operator, "const x = typeof y;");
1813
1814        no_side_effects!(test_void_operator, "const x = void 0;");
1815
1816        // delete is impure (modifies object)
1817        side_effects!(test_delete_expression, "delete obj.prop;");
1818
1819        no_side_effects!(test_sequence_expression_pure, "const x = (1, 2, 3);");
1820
1821        side_effects!(test_sequence_expression_impure, "const x = (foo(), 2, 3);");
1822
1823        no_side_effects!(test_arrow_with_block, "const foo = () => { return 1; };");
1824
1825        no_side_effects!(
1826            test_class_with_constructor,
1827            "class Foo { constructor() { this.x = 1; } }"
1828        );
1829
1830        no_side_effects!(test_class_extends, "class Foo extends Bar {}");
1831
1832        no_side_effects!(test_async_function, "async function foo() { return 1; }");
1833
1834        no_side_effects!(test_generator_function, "function* foo() { yield 1; }");
1835
1836        // Tagged templates are function calls, so impure by default
1837        side_effects!(test_tagged_template, "const x = tag`hello`;");
1838
1839        // String.raw is known to be pure
1840        no_side_effects!(
1841            test_tagged_template_string_raw,
1842            "const x = String.raw`hello ${world}`;"
1843        );
1844
1845        no_side_effects!(test_regex_literal, "const re = /pattern/g;");
1846
1847        no_side_effects!(test_bigint_literal, "const big = 123n;");
1848
1849        no_side_effects!(test_optional_chaining_pure, "const x = obj?.prop;");
1850
1851        // Optional chaining with a call is still a call
1852        side_effects!(test_optional_chaining_call, "const x = obj?.method();");
1853
1854        no_side_effects!(
1855            test_multiple_exports_pure,
1856            "export const a = 1;\nexport const b = 2;\nexport const c = 3;"
1857        );
1858
1859        no_side_effects!(test_export_function, "export function foo() { return 1; }");
1860
1861        no_side_effects!(test_export_class, "export class Foo {}");
1862
1863        module_evaluation_is_side_effect_free!(test_reexport, "export { foo } from 'bar';");
1864
1865        // import() is a function-like expression, we allow it
1866        module_evaluation_is_side_effect_free!(
1867            test_dynamic_import,
1868            "const mod = import('./module');"
1869        );
1870
1871        module_evaluation_is_side_effect_free!(
1872            test_dynamic_import_with_await,
1873            "const mod = await import('./module');"
1874        );
1875
1876        no_side_effects!(test_export_default_expression, "export default 1 + 2;");
1877
1878        side_effects!(
1879            test_export_default_expression_with_side_effect,
1880            "export default foo();"
1881        );
1882
1883        no_side_effects!(
1884            test_export_default_function,
1885            "export default function() { return 1; }"
1886        );
1887
1888        no_side_effects!(test_export_default_class, "export default class Foo {}");
1889
1890        no_side_effects!(
1891            test_export_named_with_pure_builtin,
1892            "export const result = Math.abs(-5);"
1893        );
1894
1895        side_effects!(
1896            test_multiple_exports_mixed,
1897            "export const a = 1;\nexport const b = foo();\nexport const c = 3;"
1898        );
1899    }
1900
1901    mod pure_constructors_tests {
1902        use super::*;
1903
1904        no_side_effects!(test_new_set, "const s = new Set();");
1905
1906        no_side_effects!(test_new_map, "const m = new Map();");
1907
1908        no_side_effects!(test_new_weakset, "const ws = new WeakSet();");
1909
1910        no_side_effects!(test_new_weakmap, "const wm = new WeakMap();");
1911
1912        no_side_effects!(test_new_regexp, "const re = new RegExp('pattern');");
1913
1914        no_side_effects!(test_new_date, "const d = new Date();");
1915
1916        no_side_effects!(test_new_error, "const e = new Error('message');");
1917
1918        no_side_effects!(test_new_promise, "const p = new Promise(() => {});");
1919        side_effects!(
1920            test_new_promise_effectful,
1921            "const p = new Promise(() => {console.log('hello')});"
1922        );
1923
1924        no_side_effects!(test_new_array, "const arr = new Array(10);");
1925
1926        no_side_effects!(test_new_object, "const obj = new Object();");
1927
1928        no_side_effects!(test_new_typed_array, "const arr = new Uint8Array(10);");
1929
1930        no_side_effects!(test_new_url, "const url = new URL('https://example.com');");
1931
1932        no_side_effects!(
1933            test_new_url_search_params,
1934            "const params = new URLSearchParams();"
1935        );
1936
1937        // Pure constructor with impure arguments is impure
1938        side_effects!(
1939            test_pure_constructor_with_impure_args,
1940            "const s = new Set([foo()]);"
1941        );
1942
1943        no_side_effects!(
1944            test_multiple_pure_constructors,
1945            "const s = new Set();\nconst m = new Map();\nconst re = new RegExp('test');"
1946        );
1947
1948        // Unknown constructors are impure
1949        side_effects!(
1950            test_unknown_constructor,
1951            "const custom = new CustomClass();"
1952        );
1953
1954        side_effects!(
1955            test_mixed_constructors,
1956            "const s = new Set();\nconst custom = new CustomClass();\nconst m = new Map();"
1957        );
1958    }
1959
1960    mod shadowing_detection_tests {
1961        use super::*;
1962
1963        // Math is shadowed by a local variable, so Math.abs is not the built-in
1964        side_effects!(
1965            test_shadowed_math,
1966            r#"
1967            const Math = { abs: () => console.log('side effect') };
1968            const result = Math.abs(-5);
1969            "#
1970        );
1971
1972        // Object is shadowed at top level, so Object.keys is not the built-in
1973        side_effects!(
1974            test_shadowed_object,
1975            r#"
1976            const Object = { keys: () => sideEffect() };
1977            const result = Object.keys({});
1978            "#
1979        );
1980
1981        // Array is shadowed at top level by a local class
1982        side_effects!(
1983            test_shadowed_array_constructor,
1984            r#"
1985            const Array = class { constructor() { sideEffect(); } };
1986            const arr = new Array();
1987            "#
1988        );
1989
1990        // Set is shadowed at top level
1991        side_effects!(
1992            test_shadowed_set_constructor,
1993            r#"
1994            const Set = class { constructor() { sideEffect(); } };
1995            const s = new Set();
1996            "#
1997        );
1998
1999        // Map is shadowed in a block scope
2000        side_effects!(
2001            test_shadowed_map_constructor,
2002            r#"
2003            {
2004                const Map = class { constructor() { sideEffect(); } };
2005                const m = new Map();
2006            }
2007            "#
2008        );
2009
2010        // Math is NOT shadowed here, so Math.abs is the built-in
2011        no_side_effects!(
2012            test_global_math_not_shadowed,
2013            r#"
2014            const result = Math.abs(-5);
2015            "#
2016        );
2017
2018        // Object is NOT shadowed, so Object.keys is the built-in
2019        no_side_effects!(
2020            test_global_object_not_shadowed,
2021            r#"
2022            const keys = Object.keys({ a: 1, b: 2 });
2023            "#
2024        );
2025
2026        // Array is NOT shadowed, so new Array() is the built-in
2027        no_side_effects!(
2028            test_global_array_constructor_not_shadowed,
2029            r#"
2030            const arr = new Array(1, 2, 3);
2031            "#
2032        );
2033
2034        // If Math is imported (has a non-empty ctxt), it's not the global
2035        side_effects!(
2036            test_shadowed_by_import,
2037            r#"
2038            import { Math } from './custom-math';
2039            const result = Math.abs(-5);
2040            "#
2041        );
2042
2043        // Math is shadowed in a block scope at top level
2044        side_effects!(
2045            test_nested_scope_shadowing,
2046            r#"
2047            {
2048                const Math = { floor: () => sideEffect() };
2049                const result = Math.floor(4.5);
2050            }
2051            "#
2052        );
2053
2054        // This test shows that function declarations are pure at top level
2055        // even if they have shadowed parameters. The side effect only occurs
2056        // if the function is actually called.
2057        no_side_effects!(
2058            test_parameter_shadowing,
2059            r#"
2060            function test(RegExp) {
2061                return new RegExp('test');
2062            }
2063            "#
2064        );
2065
2066        // Number is shadowed by a var declaration
2067        side_effects!(
2068            test_shadowing_with_var,
2069            r#"
2070            var Number = { isNaN: () => sideEffect() };
2071            const check = Number.isNaN(123);
2072            "#
2073        );
2074
2075        // RegExp is NOT shadowed, constructor is pure
2076        no_side_effects!(
2077            test_global_regexp_not_shadowed,
2078            r#"
2079            const re = new RegExp('[a-z]+');
2080            "#
2081        );
2082    }
2083
2084    mod literal_receiver_methods_tests {
2085        use super::*;
2086
2087        // String literal methods
2088        no_side_effects!(
2089            test_string_literal_to_lower_case,
2090            r#"const result = "HELLO".toLowerCase();"#
2091        );
2092
2093        no_side_effects!(
2094            test_string_literal_to_upper_case,
2095            r#"const result = "hello".toUpperCase();"#
2096        );
2097
2098        no_side_effects!(
2099            test_string_literal_slice,
2100            r#"const result = "hello world".slice(0, 5);"#
2101        );
2102
2103        no_side_effects!(
2104            test_string_literal_split,
2105            r#"const result = "a,b,c".split(',');"#
2106        );
2107
2108        no_side_effects!(
2109            test_string_literal_trim,
2110            r#"const result = "  hello  ".trim();"#
2111        );
2112
2113        no_side_effects!(
2114            test_string_literal_replace,
2115            r#"const result = "hello".replace('h', 'H');"#
2116        );
2117
2118        no_side_effects!(
2119            test_string_literal_includes,
2120            r#"const result = "hello world".includes('world');"#
2121        );
2122
2123        // Array literal methods
2124        no_side_effects!(
2125            test_array_literal_map,
2126            r#"const result = [1, 2, 3].map(x => x * 2);"#
2127        );
2128        side_effects!(
2129            test_array_literal_map_with_effectful_callback,
2130            r#"const result = [1, 2, 3].map(x => {globalThis.something.push(x)});"#
2131        );
2132
2133        // Number literal methods - need parentheses for number literals
2134        no_side_effects!(
2135            test_number_literal_to_fixed,
2136            r#"const result = (3.14159).toFixed(2);"#
2137        );
2138
2139        no_side_effects!(
2140            test_number_literal_to_string,
2141            r#"const result = (42).toString();"#
2142        );
2143
2144        no_side_effects!(
2145            test_number_literal_to_exponential,
2146            r#"const result = (123.456).toExponential(2);"#
2147        );
2148
2149        // Boolean literal methods
2150        no_side_effects!(
2151            test_boolean_literal_to_string,
2152            r#"const result = true.toString();"#
2153        );
2154
2155        no_side_effects!(
2156            test_boolean_literal_value_of,
2157            r#"const result = false.valueOf();"#
2158        );
2159
2160        // RegExp literal methods
2161        no_side_effects!(
2162            test_regexp_literal_to_string,
2163            r#"const result = /[a-z]+/.toString();"#
2164        );
2165
2166        // Note: test() and exec() technically modify flags on the regex, but that is fine when
2167        // called on a literal.
2168        no_side_effects!(
2169            test_regexp_literal_test,
2170            r#"const result = /[a-z]+/g.test("hello");"#
2171        );
2172
2173        no_side_effects!(
2174            test_regexp_literal_exec,
2175            r#"const result = /(\d+)/g.exec("test123");"#
2176        );
2177
2178        // Array literal with impure elements - the array construction itself has side effects
2179        // because foo() is called when creating the array
2180        side_effects!(
2181            test_array_literal_with_impure_elements,
2182            r#"const result = [foo(), 2, 3].map(x => x * 2);"#
2183        );
2184
2185        // Array literal with callback that would have side effects when called
2186        // However, callbacks are just function definitions at module load time
2187        // They don't execute until runtime, so this is side-effect free at load time
2188        no_side_effects!(
2189            test_array_literal_map_with_callback,
2190            r#"const result = [1, 2, 3].map(x => x * 2);"#
2191        );
2192    }
2193
2194    mod class_expression_side_effects_tests {
2195        use super::*;
2196
2197        // Class with no extends and no static members is pure
2198        no_side_effects!(test_class_no_extends_no_static, "class Foo {}");
2199
2200        // Class with pure extends is pure
2201        no_side_effects!(test_class_pure_extends, "class Foo extends Bar {}");
2202
2203        // Class with function call in extends clause has side effects
2204        side_effects!(
2205            test_class_extends_with_call,
2206            "class Foo extends someMixinFunction() {}"
2207        );
2208
2209        // Class with complex expression in extends clause has side effects
2210        side_effects!(
2211            test_class_extends_with_complex_expr,
2212            "class Foo extends (Bar || Baz()) {}"
2213        );
2214
2215        // Class with static property initializer that calls function has side effects
2216        side_effects!(
2217            test_class_static_property_with_call,
2218            r#"
2219        class Foo {
2220            static foo = someFunction();
2221        }
2222        "#
2223        );
2224
2225        // Class with static property with pure initializer is pure
2226        no_side_effects!(
2227            test_class_static_property_pure,
2228            r#"
2229        class Foo {
2230            static foo = 42;
2231        }
2232        "#
2233        );
2234
2235        // Class with static property with array literal is pure
2236        no_side_effects!(
2237            test_class_static_property_array_literal,
2238            r#"
2239        class Foo {
2240            static foo = [1, 2, 3];
2241        }
2242        "#
2243        );
2244
2245        // Class with static block has side effects
2246        side_effects!(
2247            test_class_static_block,
2248            r#"
2249        class Foo {
2250            static {
2251                console.log("hello");
2252            }
2253        }
2254        "#
2255        );
2256
2257        no_side_effects!(
2258            test_class_static_block_empty,
2259            r#"
2260        class Foo {
2261            static {}
2262        }
2263        "#
2264        );
2265
2266        // Class with instance property is pure (doesn't execute at definition time)
2267        no_side_effects!(
2268            test_class_instance_property_with_call,
2269            r#"
2270        class Foo {
2271            foo = someFunction();
2272        }
2273        "#
2274        );
2275
2276        // Class with constructor is pure (doesn't execute at definition time)
2277        no_side_effects!(
2278            test_class_constructor_with_side_effects,
2279            r#"
2280        class Foo {
2281            constructor() {
2282                console.log("constructor");
2283            }
2284        }
2285        "#
2286        );
2287
2288        // Class with method is pure (doesn't execute at definition time)
2289        no_side_effects!(
2290            test_class_method,
2291            r#"
2292        class Foo {
2293            method() {
2294                console.log("method");
2295            }
2296        }
2297        "#
2298        );
2299
2300        // Class expression with side effects in extends
2301        side_effects!(
2302            test_class_expr_extends_with_call,
2303            "const Foo = class extends getMixin() {};"
2304        );
2305
2306        // Class expression with static property calling function
2307        side_effects!(
2308            test_class_expr_static_with_call,
2309            r#"
2310        const Foo = class {
2311            static prop = initValue();
2312        };
2313        "#
2314        );
2315
2316        // Class expression with pure static property
2317        no_side_effects!(
2318            test_class_expr_static_pure,
2319            r#"
2320        const Foo = class {
2321            static prop = "hello";
2322        };
2323        "#
2324        );
2325
2326        // Export class with side effects
2327        side_effects!(
2328            test_export_class_with_side_effects,
2329            r#"
2330        export class Foo extends getMixin() {
2331            static prop = init();
2332        }
2333        "#
2334        );
2335
2336        // Export default class with side effects
2337        side_effects!(
2338            test_export_default_class_with_side_effects,
2339            r#"
2340        export default class Foo {
2341            static { console.log("init"); }
2342        }
2343        "#
2344        );
2345
2346        // Export class without side effects
2347        no_side_effects!(
2348            test_export_class_no_side_effects,
2349            r#"
2350        export class Foo {
2351            method() {
2352                console.log("method");
2353            }
2354        }
2355        "#
2356        );
2357
2358        // Multiple static properties, some pure, some not
2359        side_effects!(
2360            test_class_mixed_static_properties,
2361            r#"
2362        class Foo {
2363            static a = 1;
2364            static b = impureCall();
2365            static c = 3;
2366        }
2367        "#
2368        );
2369
2370        // Class with pure static property using known pure built-in
2371        no_side_effects!(
2372            test_class_static_property_pure_builtin,
2373            r#"
2374        class Foo {
2375            static value = Math.abs(-5);
2376        }
2377        "#
2378        );
2379
2380        // Class with computed property name that has side effects
2381        side_effects!(
2382            test_class_computed_property_with_call,
2383            r#"
2384        class Foo {
2385            [computeName()]() {
2386                return 42;
2387            }
2388        }
2389        "#
2390        );
2391
2392        // Class with pure computed property name
2393        no_side_effects!(
2394            test_class_computed_property_pure,
2395            r#"
2396        class Foo {
2397            ['method']() {
2398                return 42;
2399            }
2400        }
2401        "#
2402        );
2403    }
2404
2405    mod complex_variable_declarations_tests {
2406        use super::*;
2407
2408        // Simple destructuring without defaults is pure
2409        no_side_effects!(test_destructure_simple, "const { foo } = obj;");
2410
2411        // Destructuring with function call in default value has side effects
2412        side_effects!(
2413            test_destructure_default_with_call,
2414            "const { foo = someFunction() } = obj;"
2415        );
2416
2417        // Destructuring with pure default value is pure
2418        no_side_effects!(test_destructure_default_pure, "const { foo = 42 } = obj;");
2419
2420        // Destructuring with array literal default is pure
2421        no_side_effects!(
2422            test_destructure_default_array_literal,
2423            "const { foo = ['hello'] } = obj;"
2424        );
2425
2426        // Destructuring with object literal default is pure
2427        no_side_effects!(
2428            test_destructure_default_object_literal,
2429            "const { foo = { bar: 'baz' } } = obj;"
2430        );
2431
2432        // Nested destructuring with default that has side effect
2433        side_effects!(
2434            test_destructure_nested_with_call,
2435            "const { a: { b = sideEffect() } } = obj;"
2436        );
2437
2438        // Array destructuring with default that has side effect
2439        side_effects!(
2440            test_array_destructure_default_with_call,
2441            "const [a, b = getDefault()] = arr;"
2442        );
2443
2444        // Array destructuring with pure default
2445        no_side_effects!(
2446            test_array_destructure_default_pure,
2447            "const [a, b = 10] = arr;"
2448        );
2449
2450        // Multiple variables, one with side effect in default
2451        side_effects!(
2452            test_multiple_destructure_mixed,
2453            "const { foo = 1, bar = compute() } = obj;"
2454        );
2455
2456        // Rest pattern is pure
2457        no_side_effects!(test_destructure_rest_pure, "const { foo, ...rest } = obj;");
2458
2459        // Complex destructuring with multiple levels
2460        side_effects!(
2461            test_destructure_complex_with_side_effect,
2462            r#"
2463        const {
2464            a,
2465            b: { c = sideEffect() },
2466            d = [1, 2, 3]
2467        } = obj;
2468        "#
2469        );
2470
2471        // Complex destructuring all pure
2472        no_side_effects!(
2473            test_destructure_complex_pure,
2474            r#"
2475        const {
2476            a,
2477            b: { c = 5 },
2478            d = [1, 2, 3]
2479        } = obj;
2480        "#
2481        );
2482
2483        // Destructuring in export with side effect
2484        side_effects!(
2485            test_export_destructure_with_side_effect,
2486            "export const { foo = init() } = obj;"
2487        );
2488
2489        // Destructuring in export without side effect
2490        no_side_effects!(
2491            test_export_destructure_pure,
2492            "export const { foo = 42 } = obj;"
2493        );
2494
2495        // Default value with known pure built-in
2496        no_side_effects!(
2497            test_destructure_default_pure_builtin,
2498            "const { foo = Math.abs(-5) } = obj;"
2499        );
2500
2501        // Default value with pure annotation
2502        no_side_effects!(
2503            test_destructure_default_pure_annotation,
2504            "const { foo = /*#__PURE__*/ compute() } = obj;"
2505        );
2506    }
2507
2508    mod decorator_side_effects_tests {
2509        use super::*;
2510
2511        // Class decorator has side effects (executes at definition time)
2512        side_effects!(
2513            test_class_decorator,
2514            r#"
2515        @decorator
2516        class Foo {}
2517        "#
2518        );
2519
2520        // Method decorator has side effects
2521        side_effects!(
2522            test_method_decorator,
2523            r#"
2524        class Foo {
2525            @decorator
2526            method() {}
2527        }
2528        "#
2529        );
2530
2531        // Property decorator has side effects
2532        side_effects!(
2533            test_property_decorator,
2534            r#"
2535        class Foo {
2536            @decorator
2537            prop = 1;
2538        }
2539        "#
2540        );
2541
2542        // Multiple decorators
2543        side_effects!(
2544            test_multiple_decorators,
2545            r#"
2546        @decorator1
2547        @decorator2
2548        class Foo {
2549            @propDecorator
2550            prop = 1;
2551
2552            @methodDecorator
2553            method() {}
2554        }
2555        "#
2556        );
2557
2558        // Decorator with arguments
2559        side_effects!(
2560            test_decorator_with_args,
2561            r#"
2562        @decorator(config())
2563        class Foo {}
2564        "#
2565        );
2566    }
2567
2568    mod additional_edge_cases_tests {
2569        use super::*;
2570
2571        // Super property access is pure
2572        no_side_effects!(
2573            test_super_property_pure,
2574            r#"
2575        class Foo extends Bar {
2576            method() {
2577                return super.parentMethod;
2578            }
2579        }
2580        "#
2581        );
2582
2583        // Super method call has side effects (but only when invoked, not at definition)
2584        no_side_effects!(
2585            test_super_call_in_method,
2586            r#"
2587        class Foo extends Bar {
2588            method() {
2589                return super.parentMethod();
2590            }
2591        }
2592        "#
2593        );
2594
2595        // import.meta is pure
2596        no_side_effects!(test_import_meta, "const url = import.meta.url;");
2597
2598        // new.target is pure (only valid inside functions/constructors)
2599        no_side_effects!(
2600            test_new_target,
2601            r#"
2602        function Foo() {
2603            console.log(new.target);
2604        }
2605        "#
2606        );
2607
2608        // JSX element has side effects (compiles to function calls)
2609        side_effects!(test_jsx_element, "const el = <div>Hello</div>;");
2610
2611        // JSX fragment has side effects
2612        side_effects!(test_jsx_fragment, "const el = <>Hello</>;");
2613
2614        // Private field access is pure
2615        no_side_effects!(
2616            test_private_field_access,
2617            r#"
2618        class Foo {
2619            #privateField = 42;
2620            method() {
2621                return this.#privateField;
2622            }
2623        }
2624        "#
2625        );
2626
2627        // Computed super property with side effect
2628        no_side_effects!(
2629            test_super_computed_property_pure,
2630            r#"
2631        class Foo extends Bar {
2632            method() {
2633                return super['prop'];
2634            }
2635        }
2636        "#
2637        );
2638
2639        // Static block with only pure statements is pure
2640        no_side_effects!(
2641            test_static_block_pure_content,
2642            r#"
2643        class Foo {
2644            static {
2645                const x = 1;
2646                const y = 2;
2647            }
2648        }
2649        "#
2650        );
2651
2652        // Static block with side effect
2653        side_effects!(
2654            test_static_block_with_side_effect_inside,
2655            r#"
2656        class Foo {
2657            static {
2658                sideEffect();
2659            }
2660        }
2661        "#
2662        );
2663
2664        // This binding is pure
2665        no_side_effects!(
2666            test_this_expression,
2667            r#"
2668        class Foo {
2669            method() {
2670                return this;
2671            }
2672        }
2673        "#
2674        );
2675
2676        // Spread in call arguments (with pure expression)
2677        no_side_effects!(
2678            test_spread_pure_in_call,
2679            "const result = Math.max(...[1, 2, 3]);"
2680        );
2681
2682        // Spread in call arguments (with side effect)
2683        side_effects!(
2684            test_spread_with_side_effect,
2685            "const result = Math.max(...getArray());"
2686        );
2687
2688        // Complex super expression
2689        no_side_effects!(
2690            test_super_complex_access,
2691            r#"
2692        class Foo extends Bar {
2693            static method() {
2694                return super.parentMethod;
2695            }
2696        }
2697        "#
2698        );
2699
2700        // Getter/setter definitions are pure
2701        no_side_effects!(
2702            test_getter_definition,
2703            r#"
2704        const obj = {
2705            get foo() {
2706                return this._foo;
2707            }
2708        };
2709        "#
2710        );
2711
2712        // Async function declaration is pure
2713        no_side_effects!(
2714            test_async_function_declaration,
2715            r#"
2716        async function foo() {
2717            return await something;
2718        }
2719        "#
2720        );
2721
2722        // Generator function declaration is pure
2723        no_side_effects!(
2724            test_generator_declaration,
2725            r#"
2726        function* foo() {
2727            yield 1;
2728            yield 2;
2729        }
2730        "#
2731        );
2732
2733        // Async generator is pure
2734        no_side_effects!(
2735            test_async_generator,
2736            r#"
2737        async function* foo() {
2738            yield await something;
2739        }
2740        "#
2741        );
2742
2743        // Using declaration (TC39 proposal) - if supported
2744        // This would need to be handled if the parser supports it
2745
2746        // Nullish coalescing with side effects in right operand
2747        side_effects!(
2748            test_nullish_coalescing_with_side_effect,
2749            "const x = a ?? sideEffect();"
2750        );
2751
2752        // Logical OR with side effects
2753        side_effects!(
2754            test_logical_or_with_side_effect,
2755            "const x = a || sideEffect();"
2756        );
2757
2758        // Logical AND with side effects
2759        side_effects!(
2760            test_logical_and_with_side_effect,
2761            "const x = a && sideEffect();"
2762        );
2763    }
2764
2765    mod common_js_modules_tests {
2766        use super::*;
2767
2768        // Writing the module's own CommonJS exports with a pure value is the CJS
2769        // equivalent of an ESM `export` and is not a module-evaluation side effect.
2770        no_side_effects!(test_common_js_exports, "exports.foo = 'a'");
2771        no_side_effects!(test_common_js_exports_module, "module.exports.foo = 'a'");
2772        no_side_effects!(test_common_js_exports_assignment, "module.exports = {}");
2773        no_side_effects!(
2774            test_common_js_function_exports,
2775            "exports.foo = function () { return 1; }; exports.bar = 2;"
2776        );
2777
2778        module_evaluation_is_side_effect_free!(
2779            test_common_js_reexport,
2780            "module.exports = require('./other');"
2781        );
2782        module_evaluation_is_side_effect_free!(
2783            test_common_js_named_reexports,
2784            "exports.a = require('./a'); exports.b = require('./b');"
2785        );
2786
2787        // a side effect in a computed value
2788        side_effects!(
2789            test_common_js_export_impure_value,
2790            "exports.foo = sideEffect();"
2791        );
2792        // a side effect in a computed export key,
2793        side_effects!(
2794            test_common_js_export_computed_side_effect,
2795            "exports[sideEffect()] = 'a';"
2796        );
2797        // writing a non-`exports` property of `module`,
2798        side_effects!(test_module_non_export_assignment, "module.foo = 'a';");
2799        // and a locally-shadowed `exports`.
2800        side_effects!(
2801            test_shadowed_exports_assignment,
2802            "let exports = {}; exports.foo = 'a';"
2803        );
2804
2805        // A getter/setter attached to the exports object makes member writes
2806        // potentially invoke an accessor, so it is conservatively flagged as a
2807        // side effect as soon as the accessor is attached.
2808        side_effects!(
2809            test_cjs_export_setter_invoked,
2810            "module.exports = { set foo(v) { sideEffect() } }; module.exports.foo = 1;"
2811        );
2812
2813        // Reassigning `module.exports` to a fresh literal keeps later member
2814        // writes pure (the common incremental-exports pattern).
2815        no_side_effects!(
2816            test_cjs_export_fresh_then_write,
2817            "module.exports = {}; module.exports.foo = 1;"
2818        );
2819        // But reassigning it to an alias (a re-export, or any non-literal) taints
2820        // it: a later `module.exports.*` write may mutate that other object, so
2821        // it is a side effect.
2822        side_effects!(
2823            test_cjs_export_reexport_then_write,
2824            "module.exports = require('./other'); module.exports.extra = 1;"
2825        );
2826        side_effects!(
2827            test_cjs_export_alias_then_write,
2828            "module.exports = other; module.exports.foo = 1;"
2829        );
2830        // A fresh literal capturing a value the module does not own also taints
2831        // it: `module.exports.g.x = 1` mutates the global, not the exports object.
2832        side_effects!(
2833            test_cjs_export_literal_capturing_global_then_write,
2834            "module.exports = { g: globalThis }; module.exports.g.x = 1;"
2835        );
2836        // The reassignment is also detected when hidden inside a top-level
2837        // comma-sequence expression rather than a standalone statement.
2838        side_effects!(
2839            test_cjs_export_reexport_then_write_sequence,
2840            "module.exports = require('./other'), module.exports.extra = 1;"
2841        );
2842        // …and when nested in a conditional/logical expression that still runs at
2843        // module evaluation (the scan descends every evaluated expression, just
2844        // not function bodies).
2845        side_effects!(
2846            test_cjs_export_reexport_in_logical_then_write,
2847            "x && (module.exports = require('./other')); module.exports.extra = 1;"
2848        );
2849        side_effects!(
2850            test_cjs_export_setter_attached_in_conditional,
2851            "x ? (module.exports = { set foo(v) { sideEffect() } }) : 0;"
2852        );
2853        // A reassignment inside a function body does not run during module
2854        // evaluation, so it is not a taint on its own.
2855        no_side_effects!(
2856            test_cjs_export_reassign_in_function_body_is_pure,
2857            "function f() { module.exports = require('./other'); } module.exports.foo = 1;"
2858        );
2859
2860        // A class `static` block executes at module evaluation (when the class
2861        // definition is evaluated), so an accessor attached to the exports object
2862        // inside one is detected — mirroring the "top level" assignment in:
2863        //   class C { static { foo = bar; } }
2864        side_effects!(
2865            test_cjs_export_setter_in_static_block,
2866            "class C { static { module.exports = { set foo(v) { sideEffect() } }; \
2867             module.exports.foo = 1; } }"
2868        );
2869        // …but a constructor body only runs when the class is instantiated, not at
2870        // module evaluation, so the same attachment there is *not* a top-level
2871        // assignment and is not detected — mirroring the "not top level"
2872        // assignment in:
2873        //   class C { constructor() { baz = quux; } }
2874        no_side_effects!(
2875            test_cjs_export_setter_in_constructor_is_pure,
2876            "class C { constructor() { module.exports = { set foo(v) { sideEffect() } }; } } \
2877             module.exports.foo = 1;"
2878        );
2879
2880        // A `static` property initializer also runs at module evaluation (when the
2881        // class definition is evaluated), so an accessor attached to the exports
2882        // object inside one is detected.
2883        side_effects!(
2884            test_cjs_export_setter_in_static_property,
2885            "class C { static x = (module.exports = { set foo(v) { sideEffect() } }); } \
2886             module.exports.foo = 1;"
2887        );
2888
2889        no_side_effects!(
2890            test_cjs_export_define_property,
2891            "Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 1;"
2892        );
2893        no_side_effects!(
2894            test_cjs_export_define_property_module_exports,
2895            "Object.defineProperty(module.exports, 'foo', { value: 1 });"
2896        );
2897        no_side_effects!(
2898            test_cjs_export_define_property_enumerable,
2899            "Object.defineProperty(exports, 'foo', { value: 1, enumerable: true });"
2900        );
2901        no_side_effects!(
2902            test_cjs_export_define_property_getter,
2903            "Object.defineProperty(exports, 'foo', { get() { return 1; } });"
2904        );
2905        no_side_effects!(
2906            test_cjs_export_define_property_getter_function,
2907            "Object.defineProperty(exports, 'foo', { get: function() { return 1; } });"
2908        );
2909        // The shape a transpiler emits for `export { a } from './a'`.
2910        module_evaluation_is_side_effect_free!(
2911            test_cjs_export_define_property_reexport_barrel,
2912            "Object.defineProperty(exports, '__esModule', { value: true }); var _a = \
2913             require('./a'); Object.defineProperty(exports, 'a', { enumerable: true, get: \
2914             function() { return _a.a; } });"
2915        );
2916        side_effects!(
2917            test_cjs_export_define_property_setter_then_write,
2918            "Object.defineProperty(exports, 'foo', { set(v) { sideEffect() } }); exports.foo = 1;"
2919        );
2920        side_effects!(
2921            test_cjs_export_define_property_getter_then_write,
2922            "Object.defineProperty(exports, 'foo', { get() { return 1; } }); exports.bar = 1;"
2923        );
2924        side_effects!(
2925            test_cjs_export_define_property_spread_descriptor_then_write,
2926            "Object.defineProperty(exports, 'foo', { ...descriptor }); exports.bar = 1;"
2927        );
2928        side_effects!(
2929            test_cjs_export_define_property_descriptor_getter,
2930            "Object.defineProperty(exports, 'foo', { get value() { return sideEffect(); } });"
2931        );
2932        side_effects!(
2933            test_cjs_export_define_property_computed_descriptor_key_then_write,
2934            "Object.defineProperty(exports, 'foo', { [key]: fn }); exports.bar = 1;"
2935        );
2936        side_effects!(
2937            test_cjs_export_define_property_nested_setter_then_write,
2938            "Object.defineProperty(exports, 'foo', { value: { set a(v) { sideEffect() } } }); \
2939             exports.foo.a = 1;"
2940        );
2941        side_effects!(
2942            test_cjs_export_define_property_computed_key,
2943            "Object.defineProperty(exports, someVar, { value: 1 });"
2944        );
2945        side_effects!(
2946            test_define_property_foreign_target,
2947            "Object.defineProperty(globalThis, 'foo', { value: 1 });"
2948        );
2949        side_effects!(
2950            test_cjs_export_define_property_impure_value,
2951            "Object.defineProperty(exports, 'foo', { value: sideEffect() });"
2952        );
2953        side_effects!(
2954            test_cjs_export_define_property_after_reexport,
2955            "module.exports = require('./other'); Object.defineProperty(module.exports, 'foo', { \
2956             value: 1 });"
2957        );
2958    }
2959
2960    mod local_variable_mutation_tests {
2961        use super::*;
2962
2963        // The motivating case: building up a `const` object/array bound to a
2964        // fresh literal before exporting it. The mutations are unobservable
2965        // during evaluation.
2966        no_side_effects!(
2967            test_const_object_build,
2968            "const config = {}; config['a'] = 'a'; config['b'] = 'b'; export default config;"
2969        );
2970        no_side_effects!(test_const_member_assignment, "const o = {}; o.a = 1;");
2971        no_side_effects!(test_const_array_index, "const a = []; a[0] = 1;");
2972        no_side_effects!(test_const_nested_member, "const o = { a: {} }; o.a.b = 1;");
2973
2974        // Boundaries that must remain side-effectful:
2975        // a `const` aliasing an imported object (the mutation hits the import),
2976        side_effects!(
2977            test_aliased_import_mutation,
2978            "import config from './config'; const c = config; c.enabled = true;"
2979        );
2980        // a `const` aliasing the global object,
2981        side_effects!(
2982            test_aliased_global_mutation,
2983            "const g = globalThis; g.shared = 1;"
2984        );
2985        // mutating an imported binding directly,
2986        side_effects!(
2987            test_imported_binding_mutation,
2988            "import obj from 'x'; obj.foo = 1;"
2989        );
2990        // a non-fresh `const` initializer (may be a shared reference),
2991        side_effects!(
2992            test_non_safe_assignment_constant_init,
2993            "const o = makeObj(); o.a = 1;"
2994        );
2995        // a `let` binding (could be reassigned to an alias; handled later),
2996        side_effects!(test_let_object_mutation, "let o = {}; o.a = 1;");
2997        // assigning a global or an undeclared variable,
2998        side_effects!(test_global_assignment, "globalThis.shared = 1;");
2999        side_effects!(test_undeclared_assignment, "leaked = 1;");
3000        // an impure assigned value,
3001        side_effects!(
3002            test_safe_assignment_constant_impure_value,
3003            "const o = {}; o.a = sideEffect();"
3004        );
3005        // a side effect in a computed key,
3006        side_effects!(
3007            test_safe_assignment_constant_computed_key_side_effect,
3008            "const o = {}; o[sideEffect()] = 1;"
3009        );
3010        // and writing a property that has a setter, which runs the setter body
3011        // (directly, or via a nested accessor object).
3012        side_effects!(
3013            test_local_setter_invoked,
3014            "const o = { set x(v) { sideEffect() } }; o.x = 1;"
3015        );
3016        side_effects!(
3017            test_local_nested_setter_invoked,
3018            "const o = {}; o.a = { set y(v) { sideEffect() } }; o.a.y = 1;"
3019        );
3020        // Attaching an accessor to a safe `const` after its (accessor-free) init
3021        // is conservatively a side effect, even without a write that invokes it.
3022        side_effects!(
3023            test_local_setter_attached_after_init,
3024            "const o = {}; o.x = { set y(v) { sideEffect() } };"
3025        );
3026        // A setter installed via `Object.defineProperty` is caught because the
3027        // call itself is a side effect (not a known-pure builtin).
3028        side_effects!(
3029            test_local_setter_via_define_property,
3030            "const o = {}; Object.defineProperty(o, 'b', { set(x) { this.a = x / 2 } }); o.b = 4;"
3031        );
3032        // An accessor attached inside a conditional/logical expression still
3033        // removes the binding from the safe set (the scan descends evaluated
3034        // expressions, not just standalone statements).
3035        side_effects!(
3036            test_local_setter_attached_in_conditional,
3037            "const o = {}; x && (o.a = { set y(v) { sideEffect() } }); o.a.y = 1;"
3038        );
3039
3040        side_effects!(
3041            test_literal_capturing_global_nested_mutation,
3042            "const box = { g: globalThis }; box.g.x = 1;"
3043        );
3044        side_effects!(
3045            test_literal_capturing_global_direct_mutation,
3046            "const box = { g: globalThis }; box.g = 1;"
3047        );
3048        side_effects!(
3049            test_literal_capturing_global_deeply_nested_mutation,
3050            "const box = { a: { b: globalThis } }; box.a.b.x = 1;"
3051        );
3052        side_effects!(
3053            test_array_literal_capturing_global_nested_mutation,
3054            "const box = [globalThis]; box[0].x = 1;"
3055        );
3056        side_effects!(
3057            test_literal_capturing_window_mutation,
3058            "const box = { w: window }; box.w.x = 1;"
3059        );
3060        side_effects!(
3061            test_literal_capturing_import_mutation,
3062            "import obj from './obj'; const box = { obj }; box.obj.foo = 1;"
3063        );
3064        side_effects!(
3065            test_literal_capturing_require_result_mutation,
3066            "const box = { dep: require('./dep') }; box.dep.x = 1;"
3067        );
3068        side_effects!(
3069            test_literal_spreading_import_mutation,
3070            "import obj from './obj'; const box = { ...obj }; box.foo.bar = 1;"
3071        );
3072        side_effects!(
3073            test_foreign_value_assigned_then_nested_mutation,
3074            "const box = {}; box.g = globalThis; box.g.x = 1;"
3075        );
3076        side_effects!(
3077            test_foreign_value_assigned_then_direct_mutation,
3078            "const box = {}; box.g = globalThis; box.a = 1;"
3079        );
3080        side_effects!(
3081            test_require_result_assigned_then_direct_mutation,
3082            "const box = {}; box.dep = require('./dep'); box.a = 1;"
3083        );
3084        no_side_effects!(
3085            test_deeply_nested_literal_mutation,
3086            "const box = { a: { b: {} } }; box.a.b.c = 1;"
3087        );
3088        no_side_effects!(
3089            test_nested_literal_assigned_then_mutated,
3090            "const box = {}; box.a = { b: {} }; box.a.b.c = 1;"
3091        );
3092    }
3093}