oxc_ecmascript/side_effects/
expressions.rs

1use oxc_ast::ast::*;
2
3use crate::{
4    ToBigInt, ToIntegerIndex, constant_evaluation::DetermineValueType, to_numeric::ToNumeric,
5    to_primitive::ToPrimitive,
6};
7
8use super::{MayHaveSideEffects, PropertyReadSideEffects, context::MayHaveSideEffectsContext};
9
10impl<'a> MayHaveSideEffects<'a> for Expression<'a> {
11    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
12        match self {
13            Expression::Identifier(ident) => ident.may_have_side_effects(ctx),
14            Expression::NumericLiteral(_)
15            | Expression::BooleanLiteral(_)
16            | Expression::StringLiteral(_)
17            | Expression::BigIntLiteral(_)
18            | Expression::NullLiteral(_)
19            | Expression::RegExpLiteral(_)
20            | Expression::MetaProperty(_)
21            | Expression::ThisExpression(_)
22            | Expression::ArrowFunctionExpression(_)
23            | Expression::FunctionExpression(_)
24            | Expression::Super(_) => false,
25            Expression::TemplateLiteral(e) => e.may_have_side_effects(ctx),
26            Expression::UnaryExpression(e) => e.may_have_side_effects(ctx),
27            Expression::LogicalExpression(e) => e.may_have_side_effects(ctx),
28            Expression::ParenthesizedExpression(e) => e.expression.may_have_side_effects(ctx),
29            Expression::ConditionalExpression(e) => {
30                if e.test.may_have_side_effects(ctx) {
31                    return true;
32                }
33                // typeof x === 'undefined' ? fallback : x
34                if is_side_effect_free_unbound_identifier_ref(&e.alternate, &e.test, false, ctx) {
35                    return e.consequent.may_have_side_effects(ctx);
36                }
37                // typeof x !== 'undefined' ? x : fallback
38                if is_side_effect_free_unbound_identifier_ref(&e.consequent, &e.test, true, ctx) {
39                    return e.alternate.may_have_side_effects(ctx);
40                }
41                e.consequent.may_have_side_effects(ctx) || e.alternate.may_have_side_effects(ctx)
42            }
43            Expression::SequenceExpression(e) => {
44                e.expressions.iter().any(|e| e.may_have_side_effects(ctx))
45            }
46            Expression::BinaryExpression(e) => e.may_have_side_effects(ctx),
47            Expression::ObjectExpression(object_expr) => {
48                object_expr.properties.iter().any(|property| property.may_have_side_effects(ctx))
49            }
50            Expression::ArrayExpression(e) => e.may_have_side_effects(ctx),
51            Expression::ClassExpression(e) => e.may_have_side_effects(ctx),
52            // NOTE: private in can throw `TypeError`
53            Expression::ChainExpression(e) => e.expression.may_have_side_effects(ctx),
54            match_member_expression!(Expression) => {
55                self.to_member_expression().may_have_side_effects(ctx)
56            }
57            Expression::CallExpression(e) => e.may_have_side_effects(ctx),
58            Expression::NewExpression(e) => e.may_have_side_effects(ctx),
59            Expression::TaggedTemplateExpression(e) => e.may_have_side_effects(ctx),
60            _ => true,
61        }
62    }
63}
64
65impl<'a> MayHaveSideEffects<'a> for IdentifierReference<'a> {
66    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
67        match self.name.as_str() {
68            "NaN" | "Infinity" | "undefined" => false,
69            // Reading global variables may have a side effect.
70            // NOTE: It should also return true when the reference might refer to a reference value created by a with statement
71            // NOTE: we ignore TDZ errors
72            _ => ctx.unknown_global_side_effects() && ctx.is_global_reference(self),
73        }
74    }
75}
76
77impl<'a> MayHaveSideEffects<'a> for TemplateLiteral<'a> {
78    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
79        self.expressions.iter().any(|e| {
80            // ToString is called for each expression.
81            // If the expression is a Symbol or ToPrimitive returns a Symbol, an error is thrown.
82            // ToPrimitive returns the value as-is for non-Object values, so we can use it instead of ToString here.
83            e.to_primitive(ctx).is_symbol() != Some(false) || e.may_have_side_effects(ctx)
84        })
85    }
86}
87
88impl<'a> MayHaveSideEffects<'a> for UnaryExpression<'a> {
89    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
90        match self.operator {
91            UnaryOperator::Delete => true,
92            UnaryOperator::Void | UnaryOperator::LogicalNot => {
93                self.argument.may_have_side_effects(ctx)
94            }
95            UnaryOperator::Typeof => {
96                if matches!(&self.argument, Expression::Identifier(_)) {
97                    false
98                } else {
99                    self.argument.may_have_side_effects(ctx)
100                }
101            }
102            UnaryOperator::UnaryPlus => {
103                // ToNumber throws an error when the argument is Symbol / BigInt / an object that
104                // returns Symbol or BigInt from ToPrimitive
105                self.argument.to_primitive(ctx).is_symbol_or_bigint() != Some(false)
106                    || self.argument.may_have_side_effects(ctx)
107            }
108            UnaryOperator::UnaryNegation | UnaryOperator::BitwiseNot => {
109                // ToNumeric throws an error when the argument is Symbol / an object that
110                // returns Symbol from ToPrimitive
111                self.argument.to_primitive(ctx).is_symbol() != Some(false)
112                    || self.argument.may_have_side_effects(ctx)
113            }
114        }
115    }
116}
117
118impl<'a> MayHaveSideEffects<'a> for BinaryExpression<'a> {
119    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
120        match self.operator {
121            BinaryOperator::Equality
122            | BinaryOperator::Inequality
123            | BinaryOperator::StrictEquality
124            | BinaryOperator::StrictInequality
125            | BinaryOperator::LessThan
126            | BinaryOperator::LessEqualThan
127            | BinaryOperator::GreaterThan
128            | BinaryOperator::GreaterEqualThan => {
129                self.left.may_have_side_effects(ctx) || self.right.may_have_side_effects(ctx)
130            }
131            BinaryOperator::Instanceof => {
132                // When the following conditions are met, instanceof won't throw `TypeError`.
133                // - the right hand side is a known global reference which is a function
134                // - the left hand side is not a proxy
135                if let Expression::Identifier(right_ident) = &self.right {
136                    let name = right_ident.name.as_str();
137                    // Any known global non-constructor functions can be allowed here.
138                    // But because non-constructor functions are not likely to be used, we ignore them.
139                    if is_known_global_constructor(name)
140                        && ctx.is_global_reference(right_ident)
141                        && !self.left.value_type(ctx).is_undetermined()
142                    {
143                        return false;
144                    }
145                }
146                // instanceof can throw `TypeError`
147                true
148            }
149            BinaryOperator::In => {
150                // in can throw `TypeError`
151                true
152            }
153            BinaryOperator::Addition => {
154                let left = self.left.to_primitive(ctx);
155                let right = self.right.to_primitive(ctx);
156                if left.is_string() == Some(true) || right.is_string() == Some(true) {
157                    // If either side is a string, ToString is called for both sides.
158                    let other_side = if left.is_string() == Some(true) { right } else { left };
159                    // ToString() for Symbols throws an error.
160                    return other_side.is_symbol() != Some(false)
161                        || self.left.may_have_side_effects(ctx)
162                        || self.right.may_have_side_effects(ctx);
163                }
164
165                let left_to_numeric_type = left.to_numeric(ctx);
166                let right_to_numeric_type = right.to_numeric(ctx);
167                if (left_to_numeric_type.is_number() && right_to_numeric_type.is_number())
168                    || (left_to_numeric_type.is_bigint() && right_to_numeric_type.is_bigint())
169                {
170                    self.left.may_have_side_effects(ctx) || self.right.may_have_side_effects(ctx)
171                } else {
172                    true
173                }
174            }
175            BinaryOperator::Subtraction
176            | BinaryOperator::Multiplication
177            | BinaryOperator::Division
178            | BinaryOperator::Remainder
179            | BinaryOperator::ShiftLeft
180            | BinaryOperator::BitwiseOR
181            | BinaryOperator::ShiftRight
182            | BinaryOperator::BitwiseXOR
183            | BinaryOperator::BitwiseAnd
184            | BinaryOperator::Exponential
185            | BinaryOperator::ShiftRightZeroFill => {
186                let left_to_numeric_type = self.left.to_numeric(ctx);
187                let right_to_numeric_type = self.right.to_numeric(ctx);
188                if left_to_numeric_type.is_bigint() && right_to_numeric_type.is_bigint() {
189                    if self.operator == BinaryOperator::ShiftRightZeroFill {
190                        true
191                    } else if matches!(
192                        self.operator,
193                        BinaryOperator::Exponential
194                            | BinaryOperator::Division
195                            | BinaryOperator::Remainder
196                    ) {
197                        if let Expression::BigIntLiteral(right) = &self.right {
198                            match self.operator {
199                                BinaryOperator::Exponential => {
200                                    right.is_negative() || self.left.may_have_side_effects(ctx)
201                                }
202                                BinaryOperator::Division | BinaryOperator::Remainder => {
203                                    right.is_zero() || self.left.may_have_side_effects(ctx)
204                                }
205                                _ => unreachable!(),
206                            }
207                        } else {
208                            true
209                        }
210                    } else {
211                        self.left.may_have_side_effects(ctx)
212                            || self.right.may_have_side_effects(ctx)
213                    }
214                } else if left_to_numeric_type.is_number() && right_to_numeric_type.is_number() {
215                    self.left.may_have_side_effects(ctx) || self.right.may_have_side_effects(ctx)
216                } else {
217                    true
218                }
219            }
220        }
221    }
222}
223
224fn is_pure_regexp(name: &str, args: &[Argument<'_>]) -> bool {
225    name == "RegExp"
226        && match args.len() {
227            0 | 1 => true,
228            2 => args[1].as_expression().is_some_and(|e| {
229                matches!(e, Expression::Identifier(_) | Expression::StringLiteral(_))
230            }),
231            _ => false,
232        }
233}
234
235#[rustfmt::skip]
236fn is_pure_global_function(name: &str) -> bool {
237    matches!(name, "decodeURI" | "decodeURIComponent" | "encodeURI" | "encodeURIComponent"
238            | "escape" | "isFinite" | "isNaN" | "parseFloat" | "parseInt")
239}
240
241#[rustfmt::skip]
242fn is_pure_call(name: &str) -> bool {
243    matches!(name, "Date" | "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
244            | "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol")
245}
246
247#[rustfmt::skip]
248fn is_pure_constructor(name: &str) -> bool {
249    matches!(name, "Set" | "Map" | "WeakSet" | "WeakMap" | "ArrayBuffer" | "Date"
250            | "Boolean" | "Error" | "EvalError" | "RangeError" | "ReferenceError"
251            | "SyntaxError" | "TypeError" | "URIError" | "Number" | "Object" | "String" | "Symbol")
252}
253
254/// Whether the name matches any known global constructors.
255///
256/// <https://tc39.es/ecma262/multipage/global-object.html#sec-constructor-properties-of-the-global-object>
257fn is_known_global_constructor(name: &str) -> bool {
258    // technically, we need to exclude the constructors that are not supported by the target
259    matches!(
260        name,
261        "AggregateError"
262            | "Array"
263            | "ArrayBuffer"
264            | "BigInt"
265            | "BigInt64Array"
266            | "BitUint64Array"
267            | "Boolean"
268            | "DataView"
269            | "Date"
270            | "Error"
271            | "EvalError"
272            | "FinalizationRegistry"
273            | "Float32Array"
274            | "Float64Array"
275            | "Function"
276            | "Int8Array"
277            | "Int16Array"
278            | "Int32Array"
279            | "Iterator"
280            | "Map"
281            | "Number"
282            | "Object"
283            | "Promise"
284            | "Proxy"
285            | "RangeError"
286            | "ReferenceError"
287            | "RegExp"
288            | "Set"
289            | "SharedArrayBuffer"
290            | "String"
291            | "Symbol"
292            | "SyntaxError"
293            | "TypeError"
294            | "Uint8Array"
295            | "Uint8ClampedArray"
296            | "Uint16Array"
297            | "Uint32Array"
298            | "URIError"
299            | "WeakMap"
300            | "WeakSet"
301    )
302}
303
304impl<'a> MayHaveSideEffects<'a> for LogicalExpression<'a> {
305    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
306        if self.left.may_have_side_effects(ctx) {
307            return true;
308        }
309        match self.operator {
310            LogicalOperator::And => {
311                // Pattern: typeof x !== 'undefined' && x
312                if is_side_effect_free_unbound_identifier_ref(&self.right, &self.left, true, ctx) {
313                    return false;
314                }
315            }
316            LogicalOperator::Or => {
317                // Pattern: typeof x === 'undefined' || x
318                if is_side_effect_free_unbound_identifier_ref(&self.right, &self.left, false, ctx) {
319                    return false;
320                }
321            }
322            LogicalOperator::Coalesce => {}
323        }
324        self.right.may_have_side_effects(ctx)
325    }
326}
327
328impl<'a> MayHaveSideEffects<'a> for ArrayExpression<'a> {
329    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
330        self.elements.iter().any(|element| element.may_have_side_effects(ctx))
331    }
332}
333
334impl<'a> MayHaveSideEffects<'a> for ArrayExpressionElement<'a> {
335    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
336        match self {
337            ArrayExpressionElement::SpreadElement(e) => match &e.argument {
338                Expression::ArrayExpression(arr) => arr.may_have_side_effects(ctx),
339                Expression::StringLiteral(_) => false,
340                Expression::TemplateLiteral(t) => t.may_have_side_effects(ctx),
341                Expression::Identifier(ident) => {
342                    // FIXME: we should treat `arguments` outside a function scope to have sideeffects
343                    !(ident.name == "arguments" && ctx.is_global_reference(ident))
344                }
345                _ => true,
346            },
347            match_expression!(ArrayExpressionElement) => {
348                self.to_expression().may_have_side_effects(ctx)
349            }
350            ArrayExpressionElement::Elision(_) => false,
351        }
352    }
353}
354
355impl<'a> MayHaveSideEffects<'a> for ObjectPropertyKind<'a> {
356    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
357        match self {
358            ObjectPropertyKind::ObjectProperty(o) => o.may_have_side_effects(ctx),
359            ObjectPropertyKind::SpreadProperty(e) => match &e.argument {
360                Expression::ArrayExpression(arr) => arr.may_have_side_effects(ctx),
361                Expression::StringLiteral(_) => false,
362                Expression::TemplateLiteral(t) => t.may_have_side_effects(ctx),
363                _ => true,
364            },
365        }
366    }
367}
368
369impl<'a> MayHaveSideEffects<'a> for ObjectProperty<'a> {
370    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
371        self.key.may_have_side_effects(ctx) || self.value.may_have_side_effects(ctx)
372    }
373}
374
375impl<'a> MayHaveSideEffects<'a> for PropertyKey<'a> {
376    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
377        match self {
378            PropertyKey::StaticIdentifier(_) | PropertyKey::PrivateIdentifier(_) => false,
379            match_expression!(PropertyKey) => {
380                // ToPropertyKey(key) throws an error when ToPrimitive(key) throws an Error
381                // But we can ignore that by using the assumption.
382                self.to_expression().may_have_side_effects(ctx)
383            }
384        }
385    }
386}
387
388impl<'a> MayHaveSideEffects<'a> for Class<'a> {
389    /// Based on <https://github.com/evanw/esbuild/blob/v0.25.0/internal/js_ast/js_ast_helpers.go#L2320>
390    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
391        if !self.decorators.is_empty() {
392            return true;
393        }
394
395        // NOTE: extending a value that is neither constructors nor null, throws an error
396        // but that error is ignored here (it is included in the assumption)
397        // Example cases: `class A extends 0 {}`, `class A extends (async function() {}) {}`
398        // Considering these cases is difficult and requires to de-opt most classes with a super class.
399        // To allow classes with a super class to be removed, we ignore this side effect.
400        if self.super_class.as_ref().is_some_and(|sup| {
401            // `(class C extends (() => {}))` is TypeError.
402            matches!(sup.without_parentheses(), Expression::ArrowFunctionExpression(_))
403                || sup.may_have_side_effects(ctx)
404        }) {
405            return true;
406        }
407
408        self.body.body.iter().any(|element| element.may_have_side_effects(ctx))
409    }
410}
411
412impl<'a> MayHaveSideEffects<'a> for ClassElement<'a> {
413    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
414        match self {
415            ClassElement::StaticBlock(block) => {
416                block.body.iter().any(|stmt| stmt.may_have_side_effects(ctx))
417            }
418            ClassElement::MethodDefinition(e) => {
419                !e.decorators.is_empty() || e.key.may_have_side_effects(ctx)
420            }
421            ClassElement::PropertyDefinition(e) => {
422                !e.decorators.is_empty()
423                    || e.key.may_have_side_effects(ctx)
424                    || (e.r#static
425                        && e.value.as_ref().is_some_and(|v| v.may_have_side_effects(ctx)))
426            }
427            ClassElement::AccessorProperty(e) => {
428                !e.decorators.is_empty() || e.key.may_have_side_effects(ctx)
429            }
430            ClassElement::TSIndexSignature(_) => false,
431        }
432    }
433}
434
435impl<'a> MayHaveSideEffects<'a> for ChainElement<'a> {
436    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
437        match self {
438            ChainElement::CallExpression(e) => e.may_have_side_effects(ctx),
439            ChainElement::TSNonNullExpression(e) => e.expression.may_have_side_effects(ctx),
440            match_member_expression!(ChainElement) => {
441                self.to_member_expression().may_have_side_effects(ctx)
442            }
443        }
444    }
445}
446
447impl<'a> MayHaveSideEffects<'a> for MemberExpression<'a> {
448    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
449        match self {
450            MemberExpression::ComputedMemberExpression(e) => e.may_have_side_effects(ctx),
451            MemberExpression::StaticMemberExpression(e) => e.may_have_side_effects(ctx),
452            MemberExpression::PrivateFieldExpression(_) => {
453                ctx.property_read_side_effects() != PropertyReadSideEffects::None
454            }
455        }
456    }
457}
458
459impl<'a> MayHaveSideEffects<'a> for StaticMemberExpression<'a> {
460    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
461        property_access_may_have_side_effects(&self.object, &self.property.name, ctx)
462    }
463}
464
465impl<'a> MayHaveSideEffects<'a> for ComputedMemberExpression<'a> {
466    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
467        match &self.expression {
468            Expression::StringLiteral(s) => {
469                property_access_may_have_side_effects(&self.object, &s.value, ctx)
470            }
471            Expression::TemplateLiteral(t) => t.single_quasi().is_some_and(|quasi| {
472                property_access_may_have_side_effects(&self.object, &quasi, ctx)
473            }),
474            Expression::NumericLiteral(n) => !n.value.to_integer_index().is_some_and(|n| {
475                !integer_index_property_access_may_have_side_effects(&self.object, n, ctx)
476            }),
477            Expression::BigIntLiteral(b) => {
478                if b.is_negative() {
479                    return true;
480                }
481                !b.to_big_int(ctx).and_then(ToIntegerIndex::to_integer_index).is_some_and(|b| {
482                    !integer_index_property_access_may_have_side_effects(&self.object, b, ctx)
483                })
484            }
485            _ => true,
486        }
487    }
488}
489
490fn property_access_may_have_side_effects<'a>(
491    object: &Expression<'a>,
492    property: &str,
493    ctx: &impl MayHaveSideEffectsContext<'a>,
494) -> bool {
495    if object.may_have_side_effects(ctx) {
496        return true;
497    }
498    if ctx.property_read_side_effects() == PropertyReadSideEffects::None {
499        return false;
500    }
501
502    match property {
503        "length" => {
504            !(matches!(object, Expression::ArrayExpression(_))
505                || object.value_type(ctx).is_string())
506        }
507        _ => true,
508    }
509}
510
511fn integer_index_property_access_may_have_side_effects<'a>(
512    object: &Expression<'a>,
513    property: u32,
514    ctx: &impl MayHaveSideEffectsContext<'a>,
515) -> bool {
516    if object.may_have_side_effects(ctx) {
517        return true;
518    }
519    if ctx.property_read_side_effects() == PropertyReadSideEffects::None {
520        return false;
521    }
522    match object {
523        Expression::StringLiteral(s) => property as usize >= s.value.encode_utf16().count(),
524        Expression::ArrayExpression(arr) => property as usize >= get_array_minimum_length(arr),
525        _ => true,
526    }
527}
528
529fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
530    arr.elements
531        .iter()
532        .map(|e| match e {
533            ArrayExpressionElement::SpreadElement(spread) => match &spread.argument {
534                Expression::ArrayExpression(arr) => get_array_minimum_length(arr),
535                Expression::StringLiteral(str) => str.value.chars().count(),
536                _ => 0,
537            },
538            _ => 1,
539        })
540        .sum()
541}
542
543// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
544impl<'a> MayHaveSideEffects<'a> for CallExpression<'a> {
545    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
546        if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
547            return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
548        }
549
550        if let Expression::Identifier(ident) = &self.callee
551            && ctx.is_global_reference(ident)
552            && let name = ident.name.as_str()
553            && (is_pure_global_function(name)
554                || is_pure_call(name)
555                || is_pure_regexp(name, &self.arguments))
556        {
557            return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
558        }
559
560        let (object, name) = match &self.callee {
561            Expression::StaticMemberExpression(member) if !member.optional => {
562                (member.object.get_identifier_reference(), member.property.name.as_str())
563            }
564            Expression::ComputedMemberExpression(member) if !member.optional => {
565                match &member.expression {
566                    Expression::StringLiteral(s) => {
567                        (member.object.get_identifier_reference(), s.value.as_str())
568                    }
569                    _ => return true,
570                }
571            }
572            _ => return true,
573        };
574
575        let Some(object) = object else { return true };
576        if !ctx.is_global_reference(object) {
577            return true;
578        }
579
580        #[rustfmt::skip]
581        let is_global = match object.name.as_str() {
582            "Array" => matches!(name, "isArray" | "of"),
583            "ArrayBuffer" => name == "isView",
584            "Date" => matches!(name, "now" | "parse" | "UTC"),
585            "Math" => matches!(name, "abs" | "acos" | "acosh" | "asin" | "asinh" | "atan" | "atan2" | "atanh"
586                    | "cbrt" | "ceil" | "clz32" | "cos" | "cosh" | "exp" | "expm1" | "floor" | "fround" | "hypot"
587                    | "imul" | "log" | "log10" | "log1p" | "log2" | "max" | "min" | "pow" | "random" | "round"
588                    | "sign" | "sin" | "sinh" | "sqrt" | "tan" | "tanh" | "trunc"),
589            "Number" => matches!(name, "isFinite" | "isInteger" | "isNaN" | "isSafeInteger" | "parseFloat" | "parseInt"),
590            "Object" => matches!(name, "create" | "getOwnPropertyDescriptor" | "getOwnPropertyDescriptors" | "getOwnPropertyNames"
591                    | "getOwnPropertySymbols" | "getPrototypeOf" | "hasOwn" | "is" | "isExtensible" | "isFrozen" | "isSealed" | "keys"),
592            "String" => matches!(name, "fromCharCode" | "fromCodePoint" | "raw"),
593            "Symbol" => matches!(name, "for" | "keyFor"),
594            "URL" => name == "canParse",
595            "Float32Array" | "Float64Array" | "Int16Array" | "Int32Array" | "Int8Array" | "Uint16Array" | "Uint32Array" | "Uint8Array" | "Uint8ClampedArray" => name == "of",
596            _ => false,
597        };
598
599        if is_global {
600            return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
601        }
602
603        true
604    }
605}
606
607// `[ValueProperties]: PURE` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
608impl<'a> MayHaveSideEffects<'a> for NewExpression<'a> {
609    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
610        if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
611            return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
612        }
613        if let Expression::Identifier(ident) = &self.callee
614            && ctx.is_global_reference(ident)
615            && let name = ident.name.as_str()
616            && (is_pure_constructor(name) || is_pure_regexp(name, &self.arguments))
617        {
618            return self.arguments.iter().any(|e| e.may_have_side_effects(ctx));
619        }
620        true
621    }
622}
623
624impl<'a> MayHaveSideEffects<'a> for TaggedTemplateExpression<'a> {
625    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
626        if ctx.manual_pure_functions(&self.tag) {
627            self.quasi.may_have_side_effects(ctx)
628        } else {
629            true
630        }
631    }
632}
633
634impl<'a> MayHaveSideEffects<'a> for Argument<'a> {
635    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
636        match self {
637            Argument::SpreadElement(e) => match &e.argument {
638                Expression::ArrayExpression(arr) => arr.may_have_side_effects(ctx),
639                Expression::StringLiteral(_) => false,
640                Expression::TemplateLiteral(t) => t.may_have_side_effects(ctx),
641                _ => true,
642            },
643            match_expression!(Argument) => self.to_expression().may_have_side_effects(ctx),
644        }
645    }
646}
647
648impl<'a> MayHaveSideEffects<'a> for AssignmentTarget<'a> {
649    /// This only checks the `Evaluation of <AssignmentTarget>`.
650    /// The sideeffect of `PutValue(<AssignmentTarget>)` is not considered here.
651    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
652        match self {
653            match_simple_assignment_target!(AssignmentTarget) => {
654                self.to_simple_assignment_target().may_have_side_effects(ctx)
655            }
656            match_assignment_target_pattern!(AssignmentTarget) => true,
657        }
658    }
659}
660
661impl<'a> MayHaveSideEffects<'a> for SimpleAssignmentTarget<'a> {
662    fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
663        match self {
664            SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => false,
665            SimpleAssignmentTarget::StaticMemberExpression(member_expr) => {
666                member_expr.object.may_have_side_effects(ctx)
667            }
668            SimpleAssignmentTarget::ComputedMemberExpression(member_expr) => {
669                member_expr.object.may_have_side_effects(ctx)
670                    || member_expr.expression.may_have_side_effects(ctx)
671            }
672            SimpleAssignmentTarget::PrivateFieldExpression(member_expr) => {
673                member_expr.object.may_have_side_effects(ctx)
674            }
675            SimpleAssignmentTarget::TSAsExpression(_)
676            | SimpleAssignmentTarget::TSNonNullExpression(_)
677            | SimpleAssignmentTarget::TSSatisfiesExpression(_)
678            | SimpleAssignmentTarget::TSTypeAssertion(_) => true,
679        }
680    }
681}
682
683/// Helper function to check if accessing an unbound identifier reference is side-effect-free based on a guard condition.
684///
685/// This function analyzes patterns like:
686/// - `typeof x === 'undefined' && x` (safe to access x in the right branch)
687/// - `typeof x !== 'undefined' || x` (safe to access x in the right branch)
688/// - `typeof x < 'u' && x` (safe to access x in the right branch)
689///
690/// Ported from: <https://github.com/evanw/esbuild/blob/d34e79e2a998c21bb71d57b92b0017ca11756912/internal/js_ast/js_ast_helpers.go#L2594-L2639>
691fn is_side_effect_free_unbound_identifier_ref<'a>(
692    value: &Expression<'a>,
693    guard_condition: &Expression<'a>,
694    mut is_yes_branch: bool,
695    ctx: &impl MayHaveSideEffectsContext<'a>,
696) -> bool {
697    let Some(ident) = value.get_identifier_reference() else {
698        return false;
699    };
700    if !ctx.is_global_reference(ident) {
701        return false;
702    }
703
704    let Expression::BinaryExpression(bin_expr) = guard_condition else {
705        return false;
706    };
707    match bin_expr.operator {
708        BinaryOperator::StrictEquality
709        | BinaryOperator::StrictInequality
710        | BinaryOperator::Equality
711        | BinaryOperator::Inequality => {
712            let (mut ty_of, mut string) = (&bin_expr.left, &bin_expr.right);
713            if matches!(ty_of, Expression::StringLiteral(_)) {
714                std::mem::swap(&mut string, &mut ty_of);
715            }
716
717            let Expression::UnaryExpression(unary) = ty_of else {
718                return false;
719            };
720            if !(unary.operator == UnaryOperator::Typeof
721                && matches!(unary.argument, Expression::Identifier(_)))
722            {
723                return false;
724            }
725
726            let Expression::StringLiteral(string) = string else {
727                return false;
728            };
729
730            let is_undefined_check = string.value == "undefined";
731            if (is_undefined_check == is_yes_branch)
732                == matches!(
733                    bin_expr.operator,
734                    BinaryOperator::Inequality | BinaryOperator::StrictInequality
735                )
736                && unary.argument.is_specific_id(&ident.name)
737            {
738                return true;
739            }
740        }
741        BinaryOperator::LessThan
742        | BinaryOperator::LessEqualThan
743        | BinaryOperator::GreaterThan
744        | BinaryOperator::GreaterEqualThan => {
745            let (mut ty_of, mut string) = (&bin_expr.left, &bin_expr.right);
746            if matches!(ty_of, Expression::StringLiteral(_)) {
747                std::mem::swap(&mut string, &mut ty_of);
748                is_yes_branch = !is_yes_branch;
749            }
750
751            let Expression::UnaryExpression(unary) = ty_of else {
752                return false;
753            };
754            if !(unary.operator == UnaryOperator::Typeof
755                && matches!(unary.argument, Expression::Identifier(_)))
756            {
757                return false;
758            }
759
760            let Expression::StringLiteral(string) = string else {
761                return false;
762            };
763            if string.value != "u" {
764                return false;
765            }
766
767            if is_yes_branch
768                == matches!(
769                    bin_expr.operator,
770                    BinaryOperator::LessThan | BinaryOperator::LessEqualThan
771                )
772                && unary.argument.is_specific_id(&ident.name)
773            {
774                return true;
775            }
776        }
777        _ => {}
778    }
779
780    false
781}