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                if self.is_pure_annotated(call.span) || self.is_known_pure_builtin(&call.callee) {
1022                    // For known pure builtins, we need to check both:
1023                    // 1. The receiver (e.g., the array in [foo(), 2, 3].map(...))
1024                    // 2. The arguments
1025
1026                    // Check the receiver
1027                    call.callee.visit_with(self);
1028
1029                    // Check all arguments
1030                    // Assume that any function expressions in the arguments will be invoked.
1031                    self.with_will_invoke_fn_exprs(true, |this| {
1032                        call.args.visit_children_with(this);
1033                    });
1034                } else if self.is_require_or_import(&call.callee) {
1035                    self.has_imports = true;
1036                    // It would be weird to have a side effect in a require(...) statement, but not
1037                    // impossible.
1038                    call.args.visit_children_with(self);
1039                } else if self.define_property_target_is_pure(call) {
1040                    call.args.visit_children_with(self);
1041                } else {
1042                    // Unmarked calls are considered to have side effects
1043                    self.mark_side_effect();
1044                }
1045            }
1046            Expr::New(new) => {
1047                // Check for /*#__PURE__*/ annotation or known pure constructor
1048                if self.is_pure_annotated(new.span) || self.is_known_pure_constructor(&new.callee) {
1049                    // Pure constructor, but still need to check arguments
1050                    self.with_will_invoke_fn_exprs(true, |this| {
1051                        new.args.visit_children_with(this);
1052                    });
1053                } else {
1054                    // Unknown constructor calls are considered to have side effects
1055                    self.mark_side_effect();
1056                }
1057            }
1058            Expr::Assign(assign) => {
1059                // Assigning to the module's own CommonJS exports (`exports.x`,
1060                // `module.exports`, `module.exports.x`) is the CJS equivalent of an
1061                // ESM `export` declaration.
1062                //
1063                // Accessor handling lives in the collection passes: a binding (or
1064                // the exports object) that ever has a getter/setter attached to it
1065                // is excluded up front, so `assign_target_is_pure` already returns
1066                // false for member writes that could invoke one.
1067                if assign.op == AssignOp::Assign && self.assign_target_is_pure(&assign.left) {
1068                    // Still check the assigned value, and the target's computed
1069                    // property keys (e.g. `exports[sideEffect()] = …`).
1070                    assign.left.visit_with(self);
1071                    assign.right.visit_with(self);
1072                } else {
1073                    self.mark_side_effect();
1074                }
1075            }
1076            Expr::Update(_) => {
1077                // Updates (++, --) have side effects
1078                // TODO: allow updates to module level variables
1079                self.mark_side_effect();
1080            }
1081            Expr::Await(e) => {
1082                e.arg.visit_with(self);
1083            }
1084            Expr::Yield(e) => {
1085                e.arg.visit_with(self);
1086            }
1087            Expr::TaggedTpl(tagged_tpl)
1088                // Tagged template literals are function calls
1089                // But some are known to be pure, like String.raw
1090                if self.is_known_pure_builtin_function(&tagged_tpl.tag) => {
1091                    for arg in &tagged_tpl.tpl.exprs {
1092                        arg.visit_with(self);
1093                    }
1094                }
1095            Expr::OptChain(opt_chain) => {
1096                // Optional chaining can be pure if it's just member access
1097                // But if it's an optional call, it has side effects
1098                opt_chain.base.visit_with(self);
1099            }
1100            Expr::Seq(seq) => {
1101                // Sequence expressions - check each expression
1102                seq.exprs.visit_children_with(self);
1103            }
1104            Expr::SuperProp(super_prop) => {
1105                // Super property access is pure (reading from parent class)
1106                // Check if the property expression has side effects
1107                super_prop.prop.visit_with(self);
1108            }
1109            Expr::MetaProp(_) => {
1110                // Meta properties like import.meta and new.target are pure
1111                // They just read metadata, don't cause side effects
1112            }
1113            Expr::JSXMember(_) | Expr::JSXNamespacedName(_) | Expr::JSXEmpty(_) => {
1114                // JSX member expressions and names are pure (they're just identifiers)
1115            }
1116            Expr::JSXElement(_) | Expr::JSXFragment(_) => {
1117                // JSX elements compile to function calls (React.createElement, etc.)
1118                // These are side effect free but we don't technically know at this point that it is
1119                // react (could be solid or qwik or millionjs).  In any case it doesn't matter too
1120                // much since it is weird to construct jsx at the module scope.
1121                self.mark_side_effect();
1122            }
1123            Expr::PrivateName(_) => {
1124                // Private names are pure (just identifiers)
1125            }
1126
1127            // Be conservative for other expression types and just assume they are effectful
1128            _ => {
1129                self.mark_side_effect();
1130            }
1131        }
1132    }
1133
1134    fn visit_opt_chain_base(&mut self, base: &OptChainBase) {
1135        check_side_effects!(self);
1136
1137        match base {
1138            OptChainBase::Member(member) => {
1139                member.visit_with(self);
1140            }
1141            OptChainBase::Call(_opt_call) => {
1142                // Optional calls are still calls, so impure
1143                // We could maybe support some of these `(foo_enabled? undefined :
1144                // [])?.map(...)` but this seems pretty theoretical
1145                self.mark_side_effect();
1146            }
1147        }
1148    }
1149
1150    fn visit_prop_or_spread(&mut self, prop: &PropOrSpread) {
1151        check_side_effects!(self);
1152
1153        match prop {
1154            PropOrSpread::Spread(spread) => {
1155                spread.expr.visit_with(self);
1156            }
1157            PropOrSpread::Prop(prop) => {
1158                prop.visit_with(self);
1159            }
1160        }
1161    }
1162
1163    fn visit_prop(&mut self, prop: &Prop) {
1164        check_side_effects!(self);
1165
1166        match prop {
1167            Prop::KeyValue(kv) => {
1168                kv.key.visit_with(self);
1169                kv.value.visit_with(self);
1170            }
1171            Prop::Getter(getter) => {
1172                getter.key.visit_with(self);
1173                // Body is not executed at definition time
1174            }
1175            Prop::Setter(setter) => {
1176                setter.key.visit_with(self);
1177                // Body is not executed at definition time
1178            }
1179            Prop::Method(method) => {
1180                method.key.visit_with(self);
1181                // Body is not executed at definition time
1182            }
1183            Prop::Shorthand(_) => {
1184                // Shorthand properties are pure
1185            }
1186            Prop::Assign(_) => {
1187                // Assignment properties (used in object rest/spread patterns)
1188                // are side-effect free at definition
1189            }
1190        }
1191    }
1192
1193    fn visit_prop_name(&mut self, prop_name: &PropName) {
1194        check_side_effects!(self);
1195
1196        match prop_name {
1197            PropName::Computed(computed) => {
1198                // Computed property names need evaluation
1199                computed.expr.visit_with(self);
1200            }
1201            _ => {
1202                // Other property names are pure
1203            }
1204        }
1205    }
1206
1207    fn visit_class(&mut self, class: &Class) {
1208        check_side_effects!(self);
1209
1210        // Check decorators - they execute at definition time
1211        for decorator in &class.decorators {
1212            decorator.visit_with(self);
1213        }
1214
1215        // Check the extends clause - this is evaluated at definition time
1216        if let Some(super_class) = &class.super_class {
1217            super_class.visit_with(self);
1218        }
1219
1220        // Check class body for static members
1221        for member in &class.body {
1222            member.visit_with(self);
1223        }
1224    }
1225
1226    fn visit_class_member(&mut self, member: &ClassMember) {
1227        check_side_effects!(self);
1228
1229        match member {
1230            // Static blocks execute at class definition time
1231            ClassMember::StaticBlock(block) => {
1232                // Static blocks may have side effects because they execute immediately
1233                // Check the statements in the block
1234                for stmt in &block.body.stmts {
1235                    stmt.visit_with(self);
1236                }
1237            }
1238            // Check static properties - they execute at definition time
1239            ClassMember::ClassProp(class_prop) if class_prop.is_static => {
1240                // Check decorators - they execute at definition time
1241                for decorator in &class_prop.decorators {
1242                    decorator.visit_with(self);
1243                }
1244                // Check the property key (for computed properties)
1245                class_prop.key.visit_with(self);
1246                // Check the initializer - static property initializers execute at definition time
1247                if let Some(value) = &class_prop.value {
1248                    value.visit_with(self);
1249                }
1250            }
1251            // Check computed property keys for all members
1252            ClassMember::Method(method) => {
1253                // Check decorators - they execute at definition time
1254                for decorator in &method.function.decorators {
1255                    decorator.visit_with(self);
1256                }
1257                method.key.visit_with(self);
1258                // Method bodies don't execute at definition time
1259            }
1260            ClassMember::Constructor(constructor) => {
1261                constructor.key.visit_with(self);
1262                // Constructor body doesn't execute at definition time
1263            }
1264            ClassMember::PrivateMethod(private_method) => {
1265                // Check decorators - they execute at definition time
1266                for decorator in &private_method.function.decorators {
1267                    decorator.visit_with(self);
1268                }
1269                private_method.key.visit_with(self);
1270                // Method bodies don't execute at definition time
1271            }
1272            ClassMember::ClassProp(class_prop) => {
1273                // Check decorators - they execute at definition time
1274                for decorator in &class_prop.decorators {
1275                    decorator.visit_with(self);
1276                }
1277                // For non-static properties, only check the key
1278                class_prop.key.visit_with(self);
1279                // Instance property initializers don't execute at definition time
1280            }
1281            ClassMember::PrivateProp(private_prop) => {
1282                // Check decorators - they execute at definition time
1283                for decorator in &private_prop.decorators {
1284                    decorator.visit_with(self);
1285                }
1286                private_prop.key.visit_with(self);
1287                // Instance property initializers don't execute at definition time
1288            }
1289            ClassMember::AutoAccessor(auto_accessor) if auto_accessor.is_static => {
1290                // Check decorators - they execute at definition time
1291                for decorator in &auto_accessor.decorators {
1292                    decorator.visit_with(self);
1293                }
1294                // Static auto accessors execute at definition time
1295                auto_accessor.key.visit_with(self);
1296                if let Some(value) = &auto_accessor.value {
1297                    value.visit_with(self);
1298                }
1299            }
1300            ClassMember::AutoAccessor(auto_accessor) => {
1301                // Check decorators - they execute at definition time
1302                for decorator in &auto_accessor.decorators {
1303                    decorator.visit_with(self);
1304                }
1305                // Non-static auto accessors only check the key
1306                auto_accessor.key.visit_with(self);
1307            }
1308            ClassMember::Empty(_) => {
1309                // Empty members are pure
1310            }
1311            ClassMember::TsIndexSignature(_) => {
1312                // TypeScript index signatures are pure
1313            }
1314        }
1315    }
1316
1317    fn visit_decorator(&mut self, _decorator: &Decorator) {
1318        if self.has_side_effects {
1319            return;
1320        }
1321
1322        // Decorators always have side effects because they are function calls
1323        // that execute at class/member definition time, even if they're just
1324        // identifier references (e.g., @decorator is equivalent to calling decorator())
1325        self.mark_side_effect();
1326    }
1327
1328    fn visit_pat(&mut self, pat: &Pat) {
1329        check_side_effects!(self);
1330
1331        match pat {
1332            // Object patterns with default values need checking
1333            Pat::Object(object_pat) => {
1334                for prop in &object_pat.props {
1335                    match prop {
1336                        ObjectPatProp::KeyValue(kv) => {
1337                            // Check the key (for computed properties)
1338                            kv.key.visit_with(self);
1339                            // Recursively check the value pattern
1340                            kv.value.visit_with(self);
1341                        }
1342                        ObjectPatProp::Assign(assign) => {
1343                            // Check the default value if present
1344                            if let Some(value) = &assign.value {
1345                                value.visit_with(self);
1346                            }
1347                        }
1348                        ObjectPatProp::Rest(rest) => {
1349                            // Rest patterns are pure, but check the nested pattern
1350                            rest.arg.visit_with(self);
1351                        }
1352                    }
1353                }
1354            }
1355            // Array patterns with default values need checking
1356            Pat::Array(array_pat) => {
1357                for elem in array_pat.elems.iter().flatten() {
1358                    elem.visit_with(self);
1359                }
1360            }
1361            // Assignment patterns (destructuring with defaults) need checking
1362            Pat::Assign(assign_pat) => {
1363                // Check the default value - this is evaluated if the value is undefined
1364                assign_pat.right.visit_with(self);
1365                // Also check the left side pattern
1366                assign_pat.left.visit_with(self);
1367            }
1368            // Other patterns are pure
1369            _ => {}
1370        }
1371    }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376    use swc_core::{
1377        common::{FileName, GLOBALS, Mark, SourceMap, comments::SingleThreadedComments, sync::Lrc},
1378        ecma::{
1379            ast::EsVersion,
1380            parser::{EsSyntax, Syntax, parse_file_as_program},
1381            transforms::base::resolver,
1382            visit::VisitMutWith,
1383        },
1384    };
1385
1386    use super::*;
1387
1388    /// Helper function to parse JavaScript code from a string and run the resolver
1389    fn parse_and_check_for_side_effects(code: &str, expected: ModuleSideEffects) {
1390        GLOBALS.set(&Default::default(), || {
1391            let cm = Lrc::new(SourceMap::default());
1392            let fm = cm.new_source_file(Lrc::new(FileName::Anon), code.to_string());
1393
1394            let comments = SingleThreadedComments::default();
1395            let mut errors = vec![];
1396
1397            let mut program = parse_file_as_program(
1398                &fm,
1399                Syntax::Es(EsSyntax {
1400                    jsx: true,
1401                    decorators: true,
1402                    ..Default::default()
1403                }),
1404                EsVersion::latest(),
1405                Some(&comments),
1406                &mut errors,
1407            )
1408            .expect("Failed to parse");
1409
1410            // Run the resolver to mark unresolved identifiers
1411            let unresolved_mark = Mark::new();
1412            let top_level_mark = Mark::new();
1413            program.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
1414
1415            let actual =
1416                compute_module_evaluation_side_effects(&program, &comments, unresolved_mark);
1417
1418            let msg = match expected {
1419                ModuleSideEffects::ModuleEvaluationIsSideEffectFree => {
1420                    "Expected code to have no local side effects"
1421                }
1422                ModuleSideEffects::SideEffectFree => "Expected code to be side effect free",
1423                ModuleSideEffects::SideEffectful => "Expected code to have side effects",
1424            };
1425            assert_eq!(actual, expected, "{}:\n{}", msg, code);
1426        })
1427    }
1428
1429    /// Generate a test that asserts the given code has the expected side effect status
1430    macro_rules! assert_side_effects {
1431        ($name:ident, $code:expr, $expected:expr) => {
1432            #[test]
1433            fn $name() {
1434                parse_and_check_for_side_effects($code, $expected);
1435            }
1436        };
1437    }
1438
1439    macro_rules! side_effects {
1440        ($name:ident, $code:expr) => {
1441            assert_side_effects!($name, $code, ModuleSideEffects::SideEffectful);
1442        };
1443    }
1444
1445    macro_rules! no_side_effects {
1446        ($name:ident, $code:expr) => {
1447            assert_side_effects!($name, $code, ModuleSideEffects::SideEffectFree);
1448        };
1449    }
1450    macro_rules! module_evaluation_is_side_effect_free {
1451        ($name:ident, $code:expr) => {
1452            assert_side_effects!(
1453                $name,
1454                $code,
1455                ModuleSideEffects::ModuleEvaluationIsSideEffectFree
1456            );
1457        };
1458    }
1459
1460    mod basic_tests {
1461        use super::*;
1462
1463        no_side_effects!(test_empty_program, "");
1464
1465        no_side_effects!(test_simple_const_declaration, "const x = 5;");
1466
1467        no_side_effects!(test_simple_let_declaration, "let y = 'string';");
1468
1469        no_side_effects!(test_array_literal, "const arr = [1, 2, 3];");
1470
1471        no_side_effects!(test_object_literal, "const obj = { a: 1, b: 2 };");
1472
1473        no_side_effects!(test_function_declaration, "function foo() { return 1; }");
1474
1475        no_side_effects!(
1476            test_function_expression,
1477            "const foo = function() { return 1; };"
1478        );
1479
1480        no_side_effects!(test_arrow_function, "const foo = () => 1;");
1481    }
1482
1483    mod side_effects_tests {
1484        use super::*;
1485
1486        side_effects!(test_console_log, "console.log('hello');");
1487
1488        side_effects!(test_function_call, "foo();");
1489
1490        side_effects!(test_method_call, "obj.method();");
1491
1492        side_effects!(test_assignment, "x = 5;");
1493
1494        side_effects!(test_member_assignment, "obj.prop = 5;");
1495
1496        side_effects!(test_constructor_call, "new SideEffect();");
1497
1498        side_effects!(test_update_expression, "x++;");
1499    }
1500
1501    mod pure_expressions_tests {
1502        use super::*;
1503
1504        no_side_effects!(test_binary_expression, "const x = 1 + 2;");
1505
1506        no_side_effects!(test_unary_expression, "const x = -5;");
1507
1508        no_side_effects!(test_conditional_expression, "const x = true ? 1 : 2;");
1509
1510        no_side_effects!(test_template_literal, "const x = `hello ${world}`;");
1511
1512        no_side_effects!(test_nested_object, "const obj = { a: { b: { c: 1 } } };");
1513
1514        no_side_effects!(test_nested_array, "const arr = [[1, 2], [3, 4]];");
1515    }
1516
1517    mod import_export_tests {
1518        use super::*;
1519
1520        module_evaluation_is_side_effect_free!(test_import_statement, "import x from 'y';");
1521        module_evaluation_is_side_effect_free!(test_require_statement, "const x = require('y');");
1522
1523        no_side_effects!(test_export_statement, "export default 5;");
1524
1525        no_side_effects!(test_export_const, "export const x = 5;");
1526
1527        side_effects!(
1528            test_export_const_with_side_effect,
1529            "export const x = foo();"
1530        );
1531    }
1532
1533    mod mixed_cases_tests {
1534        use super::*;
1535
1536        side_effects!(test_call_in_initializer, "const x = foo();");
1537
1538        side_effects!(test_call_in_array, "const arr = [1, foo(), 3];");
1539
1540        side_effects!(test_call_in_object, "const obj = { a: foo() };");
1541
1542        no_side_effects!(
1543            test_multiple_declarations_pure,
1544            "const x = 1;\nconst y = 2;\nconst z = 3;"
1545        );
1546
1547        side_effects!(
1548            test_multiple_declarations_with_side_effect,
1549            "const x = 1;\nfoo();\nconst z = 3;"
1550        );
1551
1552        no_side_effects!(test_class_declaration, "class Foo {}");
1553
1554        no_side_effects!(
1555            test_class_with_methods,
1556            "class Foo { method() { return 1; } }"
1557        );
1558    }
1559
1560    mod pure_annotations_tests {
1561        use super::*;
1562
1563        no_side_effects!(test_pure_annotation_function_call, "/*#__PURE__*/ foo();");
1564
1565        no_side_effects!(test_pure_annotation_with_at, "/*@__PURE__*/ foo();");
1566
1567        no_side_effects!(test_pure_annotation_constructor, "/*#__PURE__*/ new Foo();");
1568
1569        no_side_effects!(
1570            test_pure_annotation_in_variable,
1571            "const x = /*#__PURE__*/ foo();"
1572        );
1573
1574        no_side_effects!(
1575            test_pure_annotation_with_pure_args,
1576            "/*#__PURE__*/ foo(1, 2, 3);"
1577        );
1578
1579        // Even with PURE annotation, impure arguments make it impure
1580        side_effects!(
1581            test_pure_annotation_with_impure_args,
1582            "/*#__PURE__*/ foo(bar());"
1583        );
1584
1585        // Without annotation, calls are impure
1586        side_effects!(test_without_pure_annotation, "foo();");
1587
1588        no_side_effects!(
1589            test_pure_nested_in_object,
1590            "const obj = { x: /*#__PURE__*/ foo() };"
1591        );
1592
1593        no_side_effects!(test_pure_in_array, "const arr = [/*#__PURE__*/ foo()];");
1594
1595        no_side_effects!(
1596            test_multiple_pure_calls,
1597            "const x = /*#__PURE__*/ foo();\nconst y = /*#__PURE__*/ bar();"
1598        );
1599
1600        side_effects!(
1601            test_mixed_pure_and_impure,
1602            "const x = /*#__PURE__*/ foo();\nbar();\nconst z = /*#__PURE__*/ baz();"
1603        );
1604    }
1605
1606    mod known_pure_builtins_tests {
1607        use super::*;
1608
1609        no_side_effects!(test_math_abs, "const x = Math.abs(-5);");
1610
1611        no_side_effects!(test_math_floor, "const x = Math.floor(3.14);");
1612
1613        no_side_effects!(test_math_max, "const x = Math.max(1, 2, 3);");
1614
1615        no_side_effects!(test_object_keys, "const keys = Object.keys(obj);");
1616
1617        no_side_effects!(test_object_values, "const values = Object.values(obj);");
1618
1619        no_side_effects!(test_object_entries, "const entries = Object.entries(obj);");
1620
1621        no_side_effects!(test_array_is_array, "const result = Array.isArray([]);");
1622
1623        no_side_effects!(
1624            test_string_from_char_code,
1625            "const char = String.fromCharCode(65);"
1626        );
1627
1628        no_side_effects!(test_number_is_nan, "const result = Number.isNaN(x);");
1629
1630        no_side_effects!(
1631            test_multiple_math_calls,
1632            "const x = Math.abs(-5);\nconst y = Math.floor(3.14);\nconst z = Math.max(x, y);"
1633        );
1634
1635        // Even pure builtins become impure if arguments are impure
1636        side_effects!(
1637            test_pure_builtin_with_impure_arg,
1638            "const x = Math.abs(foo());"
1639        );
1640
1641        no_side_effects!(
1642            test_pure_builtin_in_expression,
1643            "const x = Math.abs(-5) + Math.floor(3.14);"
1644        );
1645
1646        side_effects!(
1647            test_mixed_builtin_and_impure,
1648            "const x = Math.abs(-5);\nfoo();\nconst z = Object.keys({});"
1649        );
1650
1651        // Accessing unknown Math properties is not in our list
1652        side_effects!(test_unknown_math_property, "const x = Math.random();");
1653
1654        // Object.assign is NOT pure (it mutates)
1655        side_effects!(test_object_assign, "Object.assign(target, source);");
1656
1657        no_side_effects!(test_array_from, "const arr = Array.from(iterable);");
1658
1659        no_side_effects!(test_global_is_nan, "const result = isNaN(value);");
1660
1661        no_side_effects!(test_global_is_finite, "const result = isFinite(value);");
1662
1663        no_side_effects!(test_global_parse_int, "const num = parseInt('42', 10);");
1664
1665        no_side_effects!(test_global_parse_float, "const num = parseFloat('3.14');");
1666
1667        no_side_effects!(
1668            test_global_decode_uri,
1669            "const decoded = decodeURI(encoded);"
1670        );
1671
1672        no_side_effects!(
1673            test_global_decode_uri_component,
1674            "const decoded = decodeURIComponent(encoded);"
1675        );
1676
1677        // String() as a function (not constructor) is pure
1678        no_side_effects!(
1679            test_global_string_constructor_as_function,
1680            "const str = String(123);"
1681        );
1682
1683        // Number() as a function (not constructor) is pure
1684        no_side_effects!(
1685            test_global_number_constructor_as_function,
1686            "const num = Number('123');"
1687        );
1688
1689        // Boolean() as a function (not constructor) is pure
1690        no_side_effects!(
1691            test_global_boolean_constructor_as_function,
1692            "const bool = Boolean(value);"
1693        );
1694
1695        // Symbol() as a function is pure
1696        no_side_effects!(
1697            test_global_symbol_constructor_as_function,
1698            "const sym = Symbol('description');"
1699        );
1700
1701        // Symbol.for() is pure
1702        no_side_effects!(test_symbol_for, "const sym = Symbol.for('description');");
1703
1704        // Symbol.keyFor() is pure
1705        no_side_effects!(
1706            test_symbol_key_for,
1707            "const description = Symbol.keyFor(sym);"
1708        );
1709
1710        // Global pure function with impure argument is impure
1711        side_effects!(
1712            test_global_pure_with_impure_arg,
1713            "const result = isNaN(foo());"
1714        );
1715
1716        // isNaN shadowed at top level
1717        side_effects!(
1718            test_shadowed_global_is_nan,
1719            r#"
1720            const isNaN = () => sideEffect();
1721            const result = isNaN(value);
1722            "#
1723        );
1724    }
1725
1726    mod edge_cases_tests {
1727        use super::*;
1728
1729        no_side_effects!(test_computed_property, "const obj = { [key]: value };");
1730
1731        side_effects!(
1732            test_computed_property_with_call,
1733            "const obj = { [foo()]: value };"
1734        );
1735
1736        no_side_effects!(test_spread_in_array, "const arr = [...other];");
1737
1738        no_side_effects!(test_spread_in_object, "const obj = { ...other };");
1739
1740        no_side_effects!(test_destructuring_assignment, "const { a, b } = obj;");
1741
1742        no_side_effects!(test_array_destructuring, "const [a, b] = arr;");
1743
1744        no_side_effects!(test_nested_ternary, "const x = a ? (b ? 1 : 2) : 3;");
1745
1746        no_side_effects!(test_logical_and, "const x = a && b;");
1747
1748        no_side_effects!(test_logical_or, "const x = a || b;");
1749
1750        no_side_effects!(test_nullish_coalescing, "const x = a ?? b;");
1751
1752        no_side_effects!(test_typeof_operator, "const x = typeof y;");
1753
1754        no_side_effects!(test_void_operator, "const x = void 0;");
1755
1756        // delete is impure (modifies object)
1757        side_effects!(test_delete_expression, "delete obj.prop;");
1758
1759        no_side_effects!(test_sequence_expression_pure, "const x = (1, 2, 3);");
1760
1761        side_effects!(test_sequence_expression_impure, "const x = (foo(), 2, 3);");
1762
1763        no_side_effects!(test_arrow_with_block, "const foo = () => { return 1; };");
1764
1765        no_side_effects!(
1766            test_class_with_constructor,
1767            "class Foo { constructor() { this.x = 1; } }"
1768        );
1769
1770        no_side_effects!(test_class_extends, "class Foo extends Bar {}");
1771
1772        no_side_effects!(test_async_function, "async function foo() { return 1; }");
1773
1774        no_side_effects!(test_generator_function, "function* foo() { yield 1; }");
1775
1776        // Tagged templates are function calls, so impure by default
1777        side_effects!(test_tagged_template, "const x = tag`hello`;");
1778
1779        // String.raw is known to be pure
1780        no_side_effects!(
1781            test_tagged_template_string_raw,
1782            "const x = String.raw`hello ${world}`;"
1783        );
1784
1785        no_side_effects!(test_regex_literal, "const re = /pattern/g;");
1786
1787        no_side_effects!(test_bigint_literal, "const big = 123n;");
1788
1789        no_side_effects!(test_optional_chaining_pure, "const x = obj?.prop;");
1790
1791        // Optional chaining with a call is still a call
1792        side_effects!(test_optional_chaining_call, "const x = obj?.method();");
1793
1794        no_side_effects!(
1795            test_multiple_exports_pure,
1796            "export const a = 1;\nexport const b = 2;\nexport const c = 3;"
1797        );
1798
1799        no_side_effects!(test_export_function, "export function foo() { return 1; }");
1800
1801        no_side_effects!(test_export_class, "export class Foo {}");
1802
1803        module_evaluation_is_side_effect_free!(test_reexport, "export { foo } from 'bar';");
1804
1805        // import() is a function-like expression, we allow it
1806        module_evaluation_is_side_effect_free!(
1807            test_dynamic_import,
1808            "const mod = import('./module');"
1809        );
1810
1811        module_evaluation_is_side_effect_free!(
1812            test_dynamic_import_with_await,
1813            "const mod = await import('./module');"
1814        );
1815
1816        no_side_effects!(test_export_default_expression, "export default 1 + 2;");
1817
1818        side_effects!(
1819            test_export_default_expression_with_side_effect,
1820            "export default foo();"
1821        );
1822
1823        no_side_effects!(
1824            test_export_default_function,
1825            "export default function() { return 1; }"
1826        );
1827
1828        no_side_effects!(test_export_default_class, "export default class Foo {}");
1829
1830        no_side_effects!(
1831            test_export_named_with_pure_builtin,
1832            "export const result = Math.abs(-5);"
1833        );
1834
1835        side_effects!(
1836            test_multiple_exports_mixed,
1837            "export const a = 1;\nexport const b = foo();\nexport const c = 3;"
1838        );
1839    }
1840
1841    mod pure_constructors_tests {
1842        use super::*;
1843
1844        no_side_effects!(test_new_set, "const s = new Set();");
1845
1846        no_side_effects!(test_new_map, "const m = new Map();");
1847
1848        no_side_effects!(test_new_weakset, "const ws = new WeakSet();");
1849
1850        no_side_effects!(test_new_weakmap, "const wm = new WeakMap();");
1851
1852        no_side_effects!(test_new_regexp, "const re = new RegExp('pattern');");
1853
1854        no_side_effects!(test_new_date, "const d = new Date();");
1855
1856        no_side_effects!(test_new_error, "const e = new Error('message');");
1857
1858        no_side_effects!(test_new_promise, "const p = new Promise(() => {});");
1859        side_effects!(
1860            test_new_promise_effectful,
1861            "const p = new Promise(() => {console.log('hello')});"
1862        );
1863
1864        no_side_effects!(test_new_array, "const arr = new Array(10);");
1865
1866        no_side_effects!(test_new_object, "const obj = new Object();");
1867
1868        no_side_effects!(test_new_typed_array, "const arr = new Uint8Array(10);");
1869
1870        no_side_effects!(test_new_url, "const url = new URL('https://example.com');");
1871
1872        no_side_effects!(
1873            test_new_url_search_params,
1874            "const params = new URLSearchParams();"
1875        );
1876
1877        // Pure constructor with impure arguments is impure
1878        side_effects!(
1879            test_pure_constructor_with_impure_args,
1880            "const s = new Set([foo()]);"
1881        );
1882
1883        no_side_effects!(
1884            test_multiple_pure_constructors,
1885            "const s = new Set();\nconst m = new Map();\nconst re = new RegExp('test');"
1886        );
1887
1888        // Unknown constructors are impure
1889        side_effects!(
1890            test_unknown_constructor,
1891            "const custom = new CustomClass();"
1892        );
1893
1894        side_effects!(
1895            test_mixed_constructors,
1896            "const s = new Set();\nconst custom = new CustomClass();\nconst m = new Map();"
1897        );
1898    }
1899
1900    mod shadowing_detection_tests {
1901        use super::*;
1902
1903        // Math is shadowed by a local variable, so Math.abs is not the built-in
1904        side_effects!(
1905            test_shadowed_math,
1906            r#"
1907            const Math = { abs: () => console.log('side effect') };
1908            const result = Math.abs(-5);
1909            "#
1910        );
1911
1912        // Object is shadowed at top level, so Object.keys is not the built-in
1913        side_effects!(
1914            test_shadowed_object,
1915            r#"
1916            const Object = { keys: () => sideEffect() };
1917            const result = Object.keys({});
1918            "#
1919        );
1920
1921        // Array is shadowed at top level by a local class
1922        side_effects!(
1923            test_shadowed_array_constructor,
1924            r#"
1925            const Array = class { constructor() { sideEffect(); } };
1926            const arr = new Array();
1927            "#
1928        );
1929
1930        // Set is shadowed at top level
1931        side_effects!(
1932            test_shadowed_set_constructor,
1933            r#"
1934            const Set = class { constructor() { sideEffect(); } };
1935            const s = new Set();
1936            "#
1937        );
1938
1939        // Map is shadowed in a block scope
1940        side_effects!(
1941            test_shadowed_map_constructor,
1942            r#"
1943            {
1944                const Map = class { constructor() { sideEffect(); } };
1945                const m = new Map();
1946            }
1947            "#
1948        );
1949
1950        // Math is NOT shadowed here, so Math.abs is the built-in
1951        no_side_effects!(
1952            test_global_math_not_shadowed,
1953            r#"
1954            const result = Math.abs(-5);
1955            "#
1956        );
1957
1958        // Object is NOT shadowed, so Object.keys is the built-in
1959        no_side_effects!(
1960            test_global_object_not_shadowed,
1961            r#"
1962            const keys = Object.keys({ a: 1, b: 2 });
1963            "#
1964        );
1965
1966        // Array is NOT shadowed, so new Array() is the built-in
1967        no_side_effects!(
1968            test_global_array_constructor_not_shadowed,
1969            r#"
1970            const arr = new Array(1, 2, 3);
1971            "#
1972        );
1973
1974        // If Math is imported (has a non-empty ctxt), it's not the global
1975        side_effects!(
1976            test_shadowed_by_import,
1977            r#"
1978            import { Math } from './custom-math';
1979            const result = Math.abs(-5);
1980            "#
1981        );
1982
1983        // Math is shadowed in a block scope at top level
1984        side_effects!(
1985            test_nested_scope_shadowing,
1986            r#"
1987            {
1988                const Math = { floor: () => sideEffect() };
1989                const result = Math.floor(4.5);
1990            }
1991            "#
1992        );
1993
1994        // This test shows that function declarations are pure at top level
1995        // even if they have shadowed parameters. The side effect only occurs
1996        // if the function is actually called.
1997        no_side_effects!(
1998            test_parameter_shadowing,
1999            r#"
2000            function test(RegExp) {
2001                return new RegExp('test');
2002            }
2003            "#
2004        );
2005
2006        // Number is shadowed by a var declaration
2007        side_effects!(
2008            test_shadowing_with_var,
2009            r#"
2010            var Number = { isNaN: () => sideEffect() };
2011            const check = Number.isNaN(123);
2012            "#
2013        );
2014
2015        // RegExp is NOT shadowed, constructor is pure
2016        no_side_effects!(
2017            test_global_regexp_not_shadowed,
2018            r#"
2019            const re = new RegExp('[a-z]+');
2020            "#
2021        );
2022    }
2023
2024    mod literal_receiver_methods_tests {
2025        use super::*;
2026
2027        // String literal methods
2028        no_side_effects!(
2029            test_string_literal_to_lower_case,
2030            r#"const result = "HELLO".toLowerCase();"#
2031        );
2032
2033        no_side_effects!(
2034            test_string_literal_to_upper_case,
2035            r#"const result = "hello".toUpperCase();"#
2036        );
2037
2038        no_side_effects!(
2039            test_string_literal_slice,
2040            r#"const result = "hello world".slice(0, 5);"#
2041        );
2042
2043        no_side_effects!(
2044            test_string_literal_split,
2045            r#"const result = "a,b,c".split(',');"#
2046        );
2047
2048        no_side_effects!(
2049            test_string_literal_trim,
2050            r#"const result = "  hello  ".trim();"#
2051        );
2052
2053        no_side_effects!(
2054            test_string_literal_replace,
2055            r#"const result = "hello".replace('h', 'H');"#
2056        );
2057
2058        no_side_effects!(
2059            test_string_literal_includes,
2060            r#"const result = "hello world".includes('world');"#
2061        );
2062
2063        // Array literal methods
2064        no_side_effects!(
2065            test_array_literal_map,
2066            r#"const result = [1, 2, 3].map(x => x * 2);"#
2067        );
2068        side_effects!(
2069            test_array_literal_map_with_effectful_callback,
2070            r#"const result = [1, 2, 3].map(x => {globalThis.something.push(x)});"#
2071        );
2072
2073        // Number literal methods - need parentheses for number literals
2074        no_side_effects!(
2075            test_number_literal_to_fixed,
2076            r#"const result = (3.14159).toFixed(2);"#
2077        );
2078
2079        no_side_effects!(
2080            test_number_literal_to_string,
2081            r#"const result = (42).toString();"#
2082        );
2083
2084        no_side_effects!(
2085            test_number_literal_to_exponential,
2086            r#"const result = (123.456).toExponential(2);"#
2087        );
2088
2089        // Boolean literal methods
2090        no_side_effects!(
2091            test_boolean_literal_to_string,
2092            r#"const result = true.toString();"#
2093        );
2094
2095        no_side_effects!(
2096            test_boolean_literal_value_of,
2097            r#"const result = false.valueOf();"#
2098        );
2099
2100        // RegExp literal methods
2101        no_side_effects!(
2102            test_regexp_literal_to_string,
2103            r#"const result = /[a-z]+/.toString();"#
2104        );
2105
2106        // Note: test() and exec() technically modify flags on the regex, but that is fine when
2107        // called on a literal.
2108        no_side_effects!(
2109            test_regexp_literal_test,
2110            r#"const result = /[a-z]+/g.test("hello");"#
2111        );
2112
2113        no_side_effects!(
2114            test_regexp_literal_exec,
2115            r#"const result = /(\d+)/g.exec("test123");"#
2116        );
2117
2118        // Array literal with impure elements - the array construction itself has side effects
2119        // because foo() is called when creating the array
2120        side_effects!(
2121            test_array_literal_with_impure_elements,
2122            r#"const result = [foo(), 2, 3].map(x => x * 2);"#
2123        );
2124
2125        // Array literal with callback that would have side effects when called
2126        // However, callbacks are just function definitions at module load time
2127        // They don't execute until runtime, so this is side-effect free at load time
2128        no_side_effects!(
2129            test_array_literal_map_with_callback,
2130            r#"const result = [1, 2, 3].map(x => x * 2);"#
2131        );
2132    }
2133
2134    mod class_expression_side_effects_tests {
2135        use super::*;
2136
2137        // Class with no extends and no static members is pure
2138        no_side_effects!(test_class_no_extends_no_static, "class Foo {}");
2139
2140        // Class with pure extends is pure
2141        no_side_effects!(test_class_pure_extends, "class Foo extends Bar {}");
2142
2143        // Class with function call in extends clause has side effects
2144        side_effects!(
2145            test_class_extends_with_call,
2146            "class Foo extends someMixinFunction() {}"
2147        );
2148
2149        // Class with complex expression in extends clause has side effects
2150        side_effects!(
2151            test_class_extends_with_complex_expr,
2152            "class Foo extends (Bar || Baz()) {}"
2153        );
2154
2155        // Class with static property initializer that calls function has side effects
2156        side_effects!(
2157            test_class_static_property_with_call,
2158            r#"
2159        class Foo {
2160            static foo = someFunction();
2161        }
2162        "#
2163        );
2164
2165        // Class with static property with pure initializer is pure
2166        no_side_effects!(
2167            test_class_static_property_pure,
2168            r#"
2169        class Foo {
2170            static foo = 42;
2171        }
2172        "#
2173        );
2174
2175        // Class with static property with array literal is pure
2176        no_side_effects!(
2177            test_class_static_property_array_literal,
2178            r#"
2179        class Foo {
2180            static foo = [1, 2, 3];
2181        }
2182        "#
2183        );
2184
2185        // Class with static block has side effects
2186        side_effects!(
2187            test_class_static_block,
2188            r#"
2189        class Foo {
2190            static {
2191                console.log("hello");
2192            }
2193        }
2194        "#
2195        );
2196
2197        no_side_effects!(
2198            test_class_static_block_empty,
2199            r#"
2200        class Foo {
2201            static {}
2202        }
2203        "#
2204        );
2205
2206        // Class with instance property is pure (doesn't execute at definition time)
2207        no_side_effects!(
2208            test_class_instance_property_with_call,
2209            r#"
2210        class Foo {
2211            foo = someFunction();
2212        }
2213        "#
2214        );
2215
2216        // Class with constructor is pure (doesn't execute at definition time)
2217        no_side_effects!(
2218            test_class_constructor_with_side_effects,
2219            r#"
2220        class Foo {
2221            constructor() {
2222                console.log("constructor");
2223            }
2224        }
2225        "#
2226        );
2227
2228        // Class with method is pure (doesn't execute at definition time)
2229        no_side_effects!(
2230            test_class_method,
2231            r#"
2232        class Foo {
2233            method() {
2234                console.log("method");
2235            }
2236        }
2237        "#
2238        );
2239
2240        // Class expression with side effects in extends
2241        side_effects!(
2242            test_class_expr_extends_with_call,
2243            "const Foo = class extends getMixin() {};"
2244        );
2245
2246        // Class expression with static property calling function
2247        side_effects!(
2248            test_class_expr_static_with_call,
2249            r#"
2250        const Foo = class {
2251            static prop = initValue();
2252        };
2253        "#
2254        );
2255
2256        // Class expression with pure static property
2257        no_side_effects!(
2258            test_class_expr_static_pure,
2259            r#"
2260        const Foo = class {
2261            static prop = "hello";
2262        };
2263        "#
2264        );
2265
2266        // Export class with side effects
2267        side_effects!(
2268            test_export_class_with_side_effects,
2269            r#"
2270        export class Foo extends getMixin() {
2271            static prop = init();
2272        }
2273        "#
2274        );
2275
2276        // Export default class with side effects
2277        side_effects!(
2278            test_export_default_class_with_side_effects,
2279            r#"
2280        export default class Foo {
2281            static { console.log("init"); }
2282        }
2283        "#
2284        );
2285
2286        // Export class without side effects
2287        no_side_effects!(
2288            test_export_class_no_side_effects,
2289            r#"
2290        export class Foo {
2291            method() {
2292                console.log("method");
2293            }
2294        }
2295        "#
2296        );
2297
2298        // Multiple static properties, some pure, some not
2299        side_effects!(
2300            test_class_mixed_static_properties,
2301            r#"
2302        class Foo {
2303            static a = 1;
2304            static b = impureCall();
2305            static c = 3;
2306        }
2307        "#
2308        );
2309
2310        // Class with pure static property using known pure built-in
2311        no_side_effects!(
2312            test_class_static_property_pure_builtin,
2313            r#"
2314        class Foo {
2315            static value = Math.abs(-5);
2316        }
2317        "#
2318        );
2319
2320        // Class with computed property name that has side effects
2321        side_effects!(
2322            test_class_computed_property_with_call,
2323            r#"
2324        class Foo {
2325            [computeName()]() {
2326                return 42;
2327            }
2328        }
2329        "#
2330        );
2331
2332        // Class with pure computed property name
2333        no_side_effects!(
2334            test_class_computed_property_pure,
2335            r#"
2336        class Foo {
2337            ['method']() {
2338                return 42;
2339            }
2340        }
2341        "#
2342        );
2343    }
2344
2345    mod complex_variable_declarations_tests {
2346        use super::*;
2347
2348        // Simple destructuring without defaults is pure
2349        no_side_effects!(test_destructure_simple, "const { foo } = obj;");
2350
2351        // Destructuring with function call in default value has side effects
2352        side_effects!(
2353            test_destructure_default_with_call,
2354            "const { foo = someFunction() } = obj;"
2355        );
2356
2357        // Destructuring with pure default value is pure
2358        no_side_effects!(test_destructure_default_pure, "const { foo = 42 } = obj;");
2359
2360        // Destructuring with array literal default is pure
2361        no_side_effects!(
2362            test_destructure_default_array_literal,
2363            "const { foo = ['hello'] } = obj;"
2364        );
2365
2366        // Destructuring with object literal default is pure
2367        no_side_effects!(
2368            test_destructure_default_object_literal,
2369            "const { foo = { bar: 'baz' } } = obj;"
2370        );
2371
2372        // Nested destructuring with default that has side effect
2373        side_effects!(
2374            test_destructure_nested_with_call,
2375            "const { a: { b = sideEffect() } } = obj;"
2376        );
2377
2378        // Array destructuring with default that has side effect
2379        side_effects!(
2380            test_array_destructure_default_with_call,
2381            "const [a, b = getDefault()] = arr;"
2382        );
2383
2384        // Array destructuring with pure default
2385        no_side_effects!(
2386            test_array_destructure_default_pure,
2387            "const [a, b = 10] = arr;"
2388        );
2389
2390        // Multiple variables, one with side effect in default
2391        side_effects!(
2392            test_multiple_destructure_mixed,
2393            "const { foo = 1, bar = compute() } = obj;"
2394        );
2395
2396        // Rest pattern is pure
2397        no_side_effects!(test_destructure_rest_pure, "const { foo, ...rest } = obj;");
2398
2399        // Complex destructuring with multiple levels
2400        side_effects!(
2401            test_destructure_complex_with_side_effect,
2402            r#"
2403        const {
2404            a,
2405            b: { c = sideEffect() },
2406            d = [1, 2, 3]
2407        } = obj;
2408        "#
2409        );
2410
2411        // Complex destructuring all pure
2412        no_side_effects!(
2413            test_destructure_complex_pure,
2414            r#"
2415        const {
2416            a,
2417            b: { c = 5 },
2418            d = [1, 2, 3]
2419        } = obj;
2420        "#
2421        );
2422
2423        // Destructuring in export with side effect
2424        side_effects!(
2425            test_export_destructure_with_side_effect,
2426            "export const { foo = init() } = obj;"
2427        );
2428
2429        // Destructuring in export without side effect
2430        no_side_effects!(
2431            test_export_destructure_pure,
2432            "export const { foo = 42 } = obj;"
2433        );
2434
2435        // Default value with known pure built-in
2436        no_side_effects!(
2437            test_destructure_default_pure_builtin,
2438            "const { foo = Math.abs(-5) } = obj;"
2439        );
2440
2441        // Default value with pure annotation
2442        no_side_effects!(
2443            test_destructure_default_pure_annotation,
2444            "const { foo = /*#__PURE__*/ compute() } = obj;"
2445        );
2446    }
2447
2448    mod decorator_side_effects_tests {
2449        use super::*;
2450
2451        // Class decorator has side effects (executes at definition time)
2452        side_effects!(
2453            test_class_decorator,
2454            r#"
2455        @decorator
2456        class Foo {}
2457        "#
2458        );
2459
2460        // Method decorator has side effects
2461        side_effects!(
2462            test_method_decorator,
2463            r#"
2464        class Foo {
2465            @decorator
2466            method() {}
2467        }
2468        "#
2469        );
2470
2471        // Property decorator has side effects
2472        side_effects!(
2473            test_property_decorator,
2474            r#"
2475        class Foo {
2476            @decorator
2477            prop = 1;
2478        }
2479        "#
2480        );
2481
2482        // Multiple decorators
2483        side_effects!(
2484            test_multiple_decorators,
2485            r#"
2486        @decorator1
2487        @decorator2
2488        class Foo {
2489            @propDecorator
2490            prop = 1;
2491
2492            @methodDecorator
2493            method() {}
2494        }
2495        "#
2496        );
2497
2498        // Decorator with arguments
2499        side_effects!(
2500            test_decorator_with_args,
2501            r#"
2502        @decorator(config())
2503        class Foo {}
2504        "#
2505        );
2506    }
2507
2508    mod additional_edge_cases_tests {
2509        use super::*;
2510
2511        // Super property access is pure
2512        no_side_effects!(
2513            test_super_property_pure,
2514            r#"
2515        class Foo extends Bar {
2516            method() {
2517                return super.parentMethod;
2518            }
2519        }
2520        "#
2521        );
2522
2523        // Super method call has side effects (but only when invoked, not at definition)
2524        no_side_effects!(
2525            test_super_call_in_method,
2526            r#"
2527        class Foo extends Bar {
2528            method() {
2529                return super.parentMethod();
2530            }
2531        }
2532        "#
2533        );
2534
2535        // import.meta is pure
2536        no_side_effects!(test_import_meta, "const url = import.meta.url;");
2537
2538        // new.target is pure (only valid inside functions/constructors)
2539        no_side_effects!(
2540            test_new_target,
2541            r#"
2542        function Foo() {
2543            console.log(new.target);
2544        }
2545        "#
2546        );
2547
2548        // JSX element has side effects (compiles to function calls)
2549        side_effects!(test_jsx_element, "const el = <div>Hello</div>;");
2550
2551        // JSX fragment has side effects
2552        side_effects!(test_jsx_fragment, "const el = <>Hello</>;");
2553
2554        // Private field access is pure
2555        no_side_effects!(
2556            test_private_field_access,
2557            r#"
2558        class Foo {
2559            #privateField = 42;
2560            method() {
2561                return this.#privateField;
2562            }
2563        }
2564        "#
2565        );
2566
2567        // Computed super property with side effect
2568        no_side_effects!(
2569            test_super_computed_property_pure,
2570            r#"
2571        class Foo extends Bar {
2572            method() {
2573                return super['prop'];
2574            }
2575        }
2576        "#
2577        );
2578
2579        // Static block with only pure statements is pure
2580        no_side_effects!(
2581            test_static_block_pure_content,
2582            r#"
2583        class Foo {
2584            static {
2585                const x = 1;
2586                const y = 2;
2587            }
2588        }
2589        "#
2590        );
2591
2592        // Static block with side effect
2593        side_effects!(
2594            test_static_block_with_side_effect_inside,
2595            r#"
2596        class Foo {
2597            static {
2598                sideEffect();
2599            }
2600        }
2601        "#
2602        );
2603
2604        // This binding is pure
2605        no_side_effects!(
2606            test_this_expression,
2607            r#"
2608        class Foo {
2609            method() {
2610                return this;
2611            }
2612        }
2613        "#
2614        );
2615
2616        // Spread in call arguments (with pure expression)
2617        no_side_effects!(
2618            test_spread_pure_in_call,
2619            "const result = Math.max(...[1, 2, 3]);"
2620        );
2621
2622        // Spread in call arguments (with side effect)
2623        side_effects!(
2624            test_spread_with_side_effect,
2625            "const result = Math.max(...getArray());"
2626        );
2627
2628        // Complex super expression
2629        no_side_effects!(
2630            test_super_complex_access,
2631            r#"
2632        class Foo extends Bar {
2633            static method() {
2634                return super.parentMethod;
2635            }
2636        }
2637        "#
2638        );
2639
2640        // Getter/setter definitions are pure
2641        no_side_effects!(
2642            test_getter_definition,
2643            r#"
2644        const obj = {
2645            get foo() {
2646                return this._foo;
2647            }
2648        };
2649        "#
2650        );
2651
2652        // Async function declaration is pure
2653        no_side_effects!(
2654            test_async_function_declaration,
2655            r#"
2656        async function foo() {
2657            return await something;
2658        }
2659        "#
2660        );
2661
2662        // Generator function declaration is pure
2663        no_side_effects!(
2664            test_generator_declaration,
2665            r#"
2666        function* foo() {
2667            yield 1;
2668            yield 2;
2669        }
2670        "#
2671        );
2672
2673        // Async generator is pure
2674        no_side_effects!(
2675            test_async_generator,
2676            r#"
2677        async function* foo() {
2678            yield await something;
2679        }
2680        "#
2681        );
2682
2683        // Using declaration (TC39 proposal) - if supported
2684        // This would need to be handled if the parser supports it
2685
2686        // Nullish coalescing with side effects in right operand
2687        side_effects!(
2688            test_nullish_coalescing_with_side_effect,
2689            "const x = a ?? sideEffect();"
2690        );
2691
2692        // Logical OR with side effects
2693        side_effects!(
2694            test_logical_or_with_side_effect,
2695            "const x = a || sideEffect();"
2696        );
2697
2698        // Logical AND with side effects
2699        side_effects!(
2700            test_logical_and_with_side_effect,
2701            "const x = a && sideEffect();"
2702        );
2703    }
2704
2705    mod common_js_modules_tests {
2706        use super::*;
2707
2708        // Writing the module's own CommonJS exports with a pure value is the CJS
2709        // equivalent of an ESM `export` and is not a module-evaluation side effect.
2710        no_side_effects!(test_common_js_exports, "exports.foo = 'a'");
2711        no_side_effects!(test_common_js_exports_module, "module.exports.foo = 'a'");
2712        no_side_effects!(test_common_js_exports_assignment, "module.exports = {}");
2713        no_side_effects!(
2714            test_common_js_function_exports,
2715            "exports.foo = function () { return 1; }; exports.bar = 2;"
2716        );
2717
2718        module_evaluation_is_side_effect_free!(
2719            test_common_js_reexport,
2720            "module.exports = require('./other');"
2721        );
2722        module_evaluation_is_side_effect_free!(
2723            test_common_js_named_reexports,
2724            "exports.a = require('./a'); exports.b = require('./b');"
2725        );
2726
2727        // a side effect in a computed value
2728        side_effects!(
2729            test_common_js_export_impure_value,
2730            "exports.foo = sideEffect();"
2731        );
2732        // a side effect in a computed export key,
2733        side_effects!(
2734            test_common_js_export_computed_side_effect,
2735            "exports[sideEffect()] = 'a';"
2736        );
2737        // writing a non-`exports` property of `module`,
2738        side_effects!(test_module_non_export_assignment, "module.foo = 'a';");
2739        // and a locally-shadowed `exports`.
2740        side_effects!(
2741            test_shadowed_exports_assignment,
2742            "let exports = {}; exports.foo = 'a';"
2743        );
2744
2745        // A getter/setter attached to the exports object makes member writes
2746        // potentially invoke an accessor, so it is conservatively flagged as a
2747        // side effect as soon as the accessor is attached.
2748        side_effects!(
2749            test_cjs_export_setter_invoked,
2750            "module.exports = { set foo(v) { sideEffect() } }; module.exports.foo = 1;"
2751        );
2752
2753        // Reassigning `module.exports` to a fresh literal keeps later member
2754        // writes pure (the common incremental-exports pattern).
2755        no_side_effects!(
2756            test_cjs_export_fresh_then_write,
2757            "module.exports = {}; module.exports.foo = 1;"
2758        );
2759        // But reassigning it to an alias (a re-export, or any non-literal) taints
2760        // it: a later `module.exports.*` write may mutate that other object, so
2761        // it is a side effect.
2762        side_effects!(
2763            test_cjs_export_reexport_then_write,
2764            "module.exports = require('./other'); module.exports.extra = 1;"
2765        );
2766        side_effects!(
2767            test_cjs_export_alias_then_write,
2768            "module.exports = other; module.exports.foo = 1;"
2769        );
2770        // A fresh literal capturing a value the module does not own also taints
2771        // it: `module.exports.g.x = 1` mutates the global, not the exports object.
2772        side_effects!(
2773            test_cjs_export_literal_capturing_global_then_write,
2774            "module.exports = { g: globalThis }; module.exports.g.x = 1;"
2775        );
2776        // The reassignment is also detected when hidden inside a top-level
2777        // comma-sequence expression rather than a standalone statement.
2778        side_effects!(
2779            test_cjs_export_reexport_then_write_sequence,
2780            "module.exports = require('./other'), module.exports.extra = 1;"
2781        );
2782        // …and when nested in a conditional/logical expression that still runs at
2783        // module evaluation (the scan descends every evaluated expression, just
2784        // not function bodies).
2785        side_effects!(
2786            test_cjs_export_reexport_in_logical_then_write,
2787            "x && (module.exports = require('./other')); module.exports.extra = 1;"
2788        );
2789        side_effects!(
2790            test_cjs_export_setter_attached_in_conditional,
2791            "x ? (module.exports = { set foo(v) { sideEffect() } }) : 0;"
2792        );
2793        // A reassignment inside a function body does not run during module
2794        // evaluation, so it is not a taint on its own.
2795        no_side_effects!(
2796            test_cjs_export_reassign_in_function_body_is_pure,
2797            "function f() { module.exports = require('./other'); } module.exports.foo = 1;"
2798        );
2799
2800        // A class `static` block executes at module evaluation (when the class
2801        // definition is evaluated), so an accessor attached to the exports object
2802        // inside one is detected — mirroring the "top level" assignment in:
2803        //   class C { static { foo = bar; } }
2804        side_effects!(
2805            test_cjs_export_setter_in_static_block,
2806            "class C { static { module.exports = { set foo(v) { sideEffect() } }; \
2807             module.exports.foo = 1; } }"
2808        );
2809        // …but a constructor body only runs when the class is instantiated, not at
2810        // module evaluation, so the same attachment there is *not* a top-level
2811        // assignment and is not detected — mirroring the "not top level"
2812        // assignment in:
2813        //   class C { constructor() { baz = quux; } }
2814        no_side_effects!(
2815            test_cjs_export_setter_in_constructor_is_pure,
2816            "class C { constructor() { module.exports = { set foo(v) { sideEffect() } }; } } \
2817             module.exports.foo = 1;"
2818        );
2819
2820        // A `static` property initializer also runs at module evaluation (when the
2821        // class definition is evaluated), so an accessor attached to the exports
2822        // object inside one is detected.
2823        side_effects!(
2824            test_cjs_export_setter_in_static_property,
2825            "class C { static x = (module.exports = { set foo(v) { sideEffect() } }); } \
2826             module.exports.foo = 1;"
2827        );
2828
2829        no_side_effects!(
2830            test_cjs_export_define_property,
2831            "Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 1;"
2832        );
2833        no_side_effects!(
2834            test_cjs_export_define_property_module_exports,
2835            "Object.defineProperty(module.exports, 'foo', { value: 1 });"
2836        );
2837        no_side_effects!(
2838            test_cjs_export_define_property_enumerable,
2839            "Object.defineProperty(exports, 'foo', { value: 1, enumerable: true });"
2840        );
2841        no_side_effects!(
2842            test_cjs_export_define_property_getter,
2843            "Object.defineProperty(exports, 'foo', { get() { return 1; } });"
2844        );
2845        no_side_effects!(
2846            test_cjs_export_define_property_getter_function,
2847            "Object.defineProperty(exports, 'foo', { get: function() { return 1; } });"
2848        );
2849        // The shape a transpiler emits for `export { a } from './a'`.
2850        module_evaluation_is_side_effect_free!(
2851            test_cjs_export_define_property_reexport_barrel,
2852            "Object.defineProperty(exports, '__esModule', { value: true }); var _a = \
2853             require('./a'); Object.defineProperty(exports, 'a', { enumerable: true, get: \
2854             function() { return _a.a; } });"
2855        );
2856        side_effects!(
2857            test_cjs_export_define_property_setter_then_write,
2858            "Object.defineProperty(exports, 'foo', { set(v) { sideEffect() } }); exports.foo = 1;"
2859        );
2860        side_effects!(
2861            test_cjs_export_define_property_getter_then_write,
2862            "Object.defineProperty(exports, 'foo', { get() { return 1; } }); exports.bar = 1;"
2863        );
2864        side_effects!(
2865            test_cjs_export_define_property_spread_descriptor_then_write,
2866            "Object.defineProperty(exports, 'foo', { ...descriptor }); exports.bar = 1;"
2867        );
2868        side_effects!(
2869            test_cjs_export_define_property_descriptor_getter,
2870            "Object.defineProperty(exports, 'foo', { get value() { return sideEffect(); } });"
2871        );
2872        side_effects!(
2873            test_cjs_export_define_property_computed_descriptor_key_then_write,
2874            "Object.defineProperty(exports, 'foo', { [key]: fn }); exports.bar = 1;"
2875        );
2876        side_effects!(
2877            test_cjs_export_define_property_nested_setter_then_write,
2878            "Object.defineProperty(exports, 'foo', { value: { set a(v) { sideEffect() } } }); \
2879             exports.foo.a = 1;"
2880        );
2881        side_effects!(
2882            test_cjs_export_define_property_computed_key,
2883            "Object.defineProperty(exports, someVar, { value: 1 });"
2884        );
2885        side_effects!(
2886            test_define_property_foreign_target,
2887            "Object.defineProperty(globalThis, 'foo', { value: 1 });"
2888        );
2889        side_effects!(
2890            test_cjs_export_define_property_impure_value,
2891            "Object.defineProperty(exports, 'foo', { value: sideEffect() });"
2892        );
2893        side_effects!(
2894            test_cjs_export_define_property_after_reexport,
2895            "module.exports = require('./other'); Object.defineProperty(module.exports, 'foo', { \
2896             value: 1 });"
2897        );
2898    }
2899
2900    mod local_variable_mutation_tests {
2901        use super::*;
2902
2903        // The motivating case: building up a `const` object/array bound to a
2904        // fresh literal before exporting it. The mutations are unobservable
2905        // during evaluation.
2906        no_side_effects!(
2907            test_const_object_build,
2908            "const config = {}; config['a'] = 'a'; config['b'] = 'b'; export default config;"
2909        );
2910        no_side_effects!(test_const_member_assignment, "const o = {}; o.a = 1;");
2911        no_side_effects!(test_const_array_index, "const a = []; a[0] = 1;");
2912        no_side_effects!(test_const_nested_member, "const o = { a: {} }; o.a.b = 1;");
2913
2914        // Boundaries that must remain side-effectful:
2915        // a `const` aliasing an imported object (the mutation hits the import),
2916        side_effects!(
2917            test_aliased_import_mutation,
2918            "import config from './config'; const c = config; c.enabled = true;"
2919        );
2920        // a `const` aliasing the global object,
2921        side_effects!(
2922            test_aliased_global_mutation,
2923            "const g = globalThis; g.shared = 1;"
2924        );
2925        // mutating an imported binding directly,
2926        side_effects!(
2927            test_imported_binding_mutation,
2928            "import obj from 'x'; obj.foo = 1;"
2929        );
2930        // a non-fresh `const` initializer (may be a shared reference),
2931        side_effects!(
2932            test_non_safe_assignment_constant_init,
2933            "const o = makeObj(); o.a = 1;"
2934        );
2935        // a `let` binding (could be reassigned to an alias; handled later),
2936        side_effects!(test_let_object_mutation, "let o = {}; o.a = 1;");
2937        // assigning a global or an undeclared variable,
2938        side_effects!(test_global_assignment, "globalThis.shared = 1;");
2939        side_effects!(test_undeclared_assignment, "leaked = 1;");
2940        // an impure assigned value,
2941        side_effects!(
2942            test_safe_assignment_constant_impure_value,
2943            "const o = {}; o.a = sideEffect();"
2944        );
2945        // a side effect in a computed key,
2946        side_effects!(
2947            test_safe_assignment_constant_computed_key_side_effect,
2948            "const o = {}; o[sideEffect()] = 1;"
2949        );
2950        // and writing a property that has a setter, which runs the setter body
2951        // (directly, or via a nested accessor object).
2952        side_effects!(
2953            test_local_setter_invoked,
2954            "const o = { set x(v) { sideEffect() } }; o.x = 1;"
2955        );
2956        side_effects!(
2957            test_local_nested_setter_invoked,
2958            "const o = {}; o.a = { set y(v) { sideEffect() } }; o.a.y = 1;"
2959        );
2960        // Attaching an accessor to a safe `const` after its (accessor-free) init
2961        // is conservatively a side effect, even without a write that invokes it.
2962        side_effects!(
2963            test_local_setter_attached_after_init,
2964            "const o = {}; o.x = { set y(v) { sideEffect() } };"
2965        );
2966        // A setter installed via `Object.defineProperty` is caught because the
2967        // call itself is a side effect (not a known-pure builtin).
2968        side_effects!(
2969            test_local_setter_via_define_property,
2970            "const o = {}; Object.defineProperty(o, 'b', { set(x) { this.a = x / 2 } }); o.b = 4;"
2971        );
2972        // An accessor attached inside a conditional/logical expression still
2973        // removes the binding from the safe set (the scan descends evaluated
2974        // expressions, not just standalone statements).
2975        side_effects!(
2976            test_local_setter_attached_in_conditional,
2977            "const o = {}; x && (o.a = { set y(v) { sideEffect() } }); o.a.y = 1;"
2978        );
2979
2980        side_effects!(
2981            test_literal_capturing_global_nested_mutation,
2982            "const box = { g: globalThis }; box.g.x = 1;"
2983        );
2984        side_effects!(
2985            test_literal_capturing_global_direct_mutation,
2986            "const box = { g: globalThis }; box.g = 1;"
2987        );
2988        side_effects!(
2989            test_literal_capturing_global_deeply_nested_mutation,
2990            "const box = { a: { b: globalThis } }; box.a.b.x = 1;"
2991        );
2992        side_effects!(
2993            test_array_literal_capturing_global_nested_mutation,
2994            "const box = [globalThis]; box[0].x = 1;"
2995        );
2996        side_effects!(
2997            test_literal_capturing_window_mutation,
2998            "const box = { w: window }; box.w.x = 1;"
2999        );
3000        side_effects!(
3001            test_literal_capturing_import_mutation,
3002            "import obj from './obj'; const box = { obj }; box.obj.foo = 1;"
3003        );
3004        side_effects!(
3005            test_literal_capturing_require_result_mutation,
3006            "const box = { dep: require('./dep') }; box.dep.x = 1;"
3007        );
3008        side_effects!(
3009            test_literal_spreading_import_mutation,
3010            "import obj from './obj'; const box = { ...obj }; box.foo.bar = 1;"
3011        );
3012        side_effects!(
3013            test_foreign_value_assigned_then_nested_mutation,
3014            "const box = {}; box.g = globalThis; box.g.x = 1;"
3015        );
3016        side_effects!(
3017            test_foreign_value_assigned_then_direct_mutation,
3018            "const box = {}; box.g = globalThis; box.a = 1;"
3019        );
3020        side_effects!(
3021            test_require_result_assigned_then_direct_mutation,
3022            "const box = {}; box.dep = require('./dep'); box.a = 1;"
3023        );
3024        no_side_effects!(
3025            test_deeply_nested_literal_mutation,
3026            "const box = { a: { b: {} } }; box.a.b.c = 1;"
3027        );
3028        no_side_effects!(
3029            test_nested_literal_assigned_then_mutated,
3030            "const box = {}; box.a = { b: {} }; box.a.b.c = 1;"
3031        );
3032    }
3033}