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