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