oxc_transformer_plugins/
replace_global_defines.rs

1use std::{cmp::Ordering, sync::Arc};
2
3use rustc_hash::FxHashSet;
4
5use oxc_allocator::{Address, Allocator, GetAddress, UnstableAddress};
6use oxc_ast::ast::*;
7use oxc_ast_visit::{VisitMut, walk_mut};
8use oxc_diagnostics::OxcDiagnostic;
9use oxc_parser::Parser;
10use oxc_semantic::{IsGlobalReference, ReferenceFlags, ScopeFlags, Scoping};
11use oxc_span::{CompactStr, SPAN, SourceType};
12use oxc_syntax::identifier::is_identifier_name;
13use oxc_traverse::{Ancestor, Traverse, traverse_mut};
14
15use crate::TraverseCtx;
16
17/// Configuration for [ReplaceGlobalDefines].
18///
19/// Due to the usage of an arena allocator, the constructor will parse once for grammatical errors,
20/// and does not save the constructed expression.
21///
22/// The data is stored in an `Arc` so this can be shared across threads.
23#[derive(Debug, Clone)]
24pub struct ReplaceGlobalDefinesConfig(Arc<ReplaceGlobalDefinesConfigImpl>);
25
26static THIS_ATOM: Atom<'static> = Atom::new_const("this");
27
28#[derive(Debug)]
29struct IdentifierDefine {
30    identifier_defines: Vec<(/* key */ CompactStr, /* value */ CompactStr)>,
31    /// Whether user want to replace `ThisExpression`, avoid linear scan for each `ThisExpression`
32    has_this_expr_define: bool,
33}
34#[derive(Debug)]
35struct ReplaceGlobalDefinesConfigImpl {
36    identifier: IdentifierDefine,
37    dot: Vec<DotDefine>,
38    meta_property: Vec<MetaPropertyDefine>,
39    /// extra field to avoid linear scan `meta_property` to check if it has `import.meta` every
40    /// time
41    /// Some(replacement): import.meta -> replacement
42    /// None -> no need to replace import.meta
43    import_meta: Option<CompactStr>,
44}
45
46#[derive(Debug)]
47pub struct DotDefine {
48    /// Member expression parts
49    pub parts: Vec<CompactStr>,
50    pub value: CompactStr,
51}
52
53#[derive(Debug)]
54pub struct MetaPropertyDefine {
55    /// only store parts after `import.meta`
56    pub parts: Vec<CompactStr>,
57    pub value: CompactStr,
58    pub postfix_wildcard: bool,
59}
60
61impl MetaPropertyDefine {
62    pub fn new(parts: Vec<CompactStr>, value: CompactStr, postfix_wildcard: bool) -> Self {
63        Self { parts, value, postfix_wildcard }
64    }
65}
66
67impl DotDefine {
68    fn new(parts: Vec<CompactStr>, value: CompactStr) -> Self {
69        Self { parts, value }
70    }
71}
72
73enum IdentifierType {
74    Identifier,
75    DotDefines { parts: Vec<CompactStr> },
76    // import.meta.a
77    ImportMetaWithParts { parts: Vec<CompactStr>, postfix_wildcard: bool },
78    // import.meta or import.meta.*
79    ImportMeta(bool),
80}
81
82impl ReplaceGlobalDefinesConfig {
83    /// # Errors
84    ///
85    /// * key is not an identifier
86    /// * value has a syntax error
87    pub fn new<S: AsRef<str>>(defines: &[(S, S)]) -> Result<Self, Vec<OxcDiagnostic>> {
88        let allocator = Allocator::default();
89        let mut identifier_defines = vec![];
90        let mut dot_defines = vec![];
91        let mut meta_properties_defines = vec![];
92        let mut import_meta = None;
93        let mut has_this_expr_define = false;
94        for (key, value) in defines {
95            let key = key.as_ref();
96
97            let value = value.as_ref();
98            Self::check_value(&allocator, value)?;
99
100            match Self::check_key(key)? {
101                IdentifierType::Identifier => {
102                    has_this_expr_define |= key == "this";
103                    identifier_defines.push((CompactStr::new(key), CompactStr::new(value)));
104                }
105                IdentifierType::DotDefines { parts } => {
106                    dot_defines.push(DotDefine::new(parts, CompactStr::new(value)));
107                }
108                IdentifierType::ImportMetaWithParts { parts, postfix_wildcard } => {
109                    meta_properties_defines.push(MetaPropertyDefine::new(
110                        parts,
111                        CompactStr::new(value),
112                        postfix_wildcard,
113                    ));
114                }
115                IdentifierType::ImportMeta(postfix_wildcard) => {
116                    if postfix_wildcard {
117                        meta_properties_defines.push(MetaPropertyDefine::new(
118                            vec![],
119                            CompactStr::new(value),
120                            postfix_wildcard,
121                        ));
122                    } else {
123                        import_meta = Some(CompactStr::new(value));
124                    }
125                }
126            }
127        }
128        // Always move specific meta define before wildcard dot define
129        // Keep other order unchanged
130        // see test case replace_global_definitions_dot_with_postfix_mixed as an example
131        meta_properties_defines.sort_by(|a, b| {
132            if !a.postfix_wildcard && b.postfix_wildcard {
133                Ordering::Less
134            } else if a.postfix_wildcard && b.postfix_wildcard {
135                Ordering::Greater
136            } else {
137                Ordering::Equal
138            }
139        });
140        Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl {
141            identifier: IdentifierDefine { identifier_defines, has_this_expr_define },
142            dot: dot_defines,
143            meta_property: meta_properties_defines,
144            import_meta,
145        })))
146    }
147
148    fn check_key(key: &str) -> Result<IdentifierType, Vec<OxcDiagnostic>> {
149        let parts: Vec<&str> = key.split('.').collect();
150
151        assert!(!parts.is_empty());
152
153        if parts.len() == 1 {
154            if !is_identifier_name(parts[0]) {
155                return Err(vec![OxcDiagnostic::error(format!(
156                    "The define key `{key}` is not an identifier."
157                ))]);
158            }
159            return Ok(IdentifierType::Identifier);
160        }
161        let normalized_parts_len =
162            if parts[parts.len() - 1] == "*" { parts.len() - 1 } else { parts.len() };
163        // We can ensure now the parts.len() >= 2
164        let is_import_meta = parts[0] == "import" && parts[1] == "meta";
165
166        for part in &parts[0..normalized_parts_len] {
167            if !is_identifier_name(part) {
168                return Err(vec![OxcDiagnostic::error(format!(
169                    "The define key `{key}` contains an invalid identifier `{part}`."
170                ))]);
171            }
172        }
173        if is_import_meta {
174            match normalized_parts_len {
175                2 => Ok(IdentifierType::ImportMeta(normalized_parts_len != parts.len())),
176                _ => Ok(IdentifierType::ImportMetaWithParts {
177                    parts: parts
178                        .iter()
179                        .skip(2)
180                        .take(normalized_parts_len - 2)
181                        .map(|s| CompactStr::new(s))
182                        .collect(),
183                    postfix_wildcard: normalized_parts_len != parts.len(),
184                }),
185            }
186        // StaticMemberExpression with postfix wildcard
187        } else if normalized_parts_len != parts.len() {
188            Err(vec![OxcDiagnostic::error(
189                "The postfix wildcard is only allowed for `import.meta`.".to_string(),
190            )])
191        } else {
192            Ok(IdentifierType::DotDefines {
193                parts: parts
194                    .iter()
195                    .take(normalized_parts_len)
196                    .map(|s| CompactStr::new(s))
197                    .collect(),
198            })
199        }
200    }
201
202    fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec<OxcDiagnostic>> {
203        Parser::new(allocator, source_text, SourceType::default()).parse_expression()?;
204        Ok(())
205    }
206}
207
208#[must_use]
209pub struct ReplaceGlobalDefinesReturn {
210    pub scoping: Scoping,
211    pub changed: bool,
212}
213
214/// Replace Global Defines.
215///
216/// References:
217///
218/// * <https://esbuild.github.io/api/#define>
219/// * <https://github.com/terser/terser?tab=readme-ov-file#conditional-compilation>
220/// * <https://github.com/evanw/esbuild/blob/9c13ae1f06dfa909eb4a53882e3b7e4216a503fe/internal/config/globals.go#L852-L1014>
221pub struct ReplaceGlobalDefines<'a> {
222    allocator: &'a Allocator,
223    config: ReplaceGlobalDefinesConfig,
224    /// Since `Traverse` did not provide a way to skipping visiting sub tree of the AstNode,
225    /// Use `Option<Address>` to lock the current node when it is `Some`.
226    /// during visiting sub tree, the `Lock` will always be `Some`, and we can early return, this
227    /// could acheieve same effect as skipping visiting sub tree.
228    /// When `exit` the node, reset the `Lock` to `None` to make sure not affect other
229    /// transformation.
230    ast_node_lock: Option<Address>,
231    changed: bool,
232}
233
234impl<'a> Traverse<'a, ()> for ReplaceGlobalDefines<'a> {
235    fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
236        if self.ast_node_lock.is_some() {
237            return;
238        }
239        let is_replaced =
240            self.replace_identifier_defines(expr, ctx) || self.replace_dot_defines(expr, ctx);
241        if is_replaced {
242            self.mark_as_changed();
243            self.ast_node_lock = Some(expr.address());
244        }
245    }
246
247    fn exit_expression(&mut self, node: &mut Expression<'a>, _ctx: &mut TraverseCtx<'a>) {
248        if self.ast_node_lock == Some(node.address()) {
249            self.ast_node_lock = None;
250        }
251    }
252
253    fn enter_assignment_expression(
254        &mut self,
255        node: &mut AssignmentExpression<'a>,
256        ctx: &mut TraverseCtx<'a>,
257    ) {
258        if self.ast_node_lock.is_some() {
259            return;
260        }
261        if self.replace_define_with_assignment_expr(node, ctx) {
262            self.mark_as_changed();
263            // `AssignmentExpression` is stored in a `Box`, so has a stable memory location
264            self.ast_node_lock = Some(node.unstable_address());
265        }
266    }
267
268    fn exit_assignment_expression(
269        &mut self,
270        node: &mut AssignmentExpression<'a>,
271        _: &mut TraverseCtx<'a>,
272    ) {
273        // `AssignmentExpression` is stored in a `Box`, so has a stable memory location
274        if self.ast_node_lock == Some(node.unstable_address()) {
275            self.ast_node_lock = None;
276        }
277    }
278}
279
280impl<'a> ReplaceGlobalDefines<'a> {
281    pub fn new(allocator: &'a Allocator, config: ReplaceGlobalDefinesConfig) -> Self {
282        Self { allocator, config, ast_node_lock: None, changed: false }
283    }
284
285    fn mark_as_changed(&mut self) {
286        self.changed = true;
287    }
288
289    pub fn build(
290        &mut self,
291        scoping: Scoping,
292        program: &mut Program<'a>,
293    ) -> ReplaceGlobalDefinesReturn {
294        let scoping = traverse_mut(self, self.allocator, program, scoping, ());
295        ReplaceGlobalDefinesReturn { scoping, changed: self.changed }
296    }
297
298    // Construct a new expression because we don't have ast clone right now.
299    fn parse_value(&self, source_text: &str, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
300        // Allocate the string lazily because replacement happens rarely.
301        let source_text = self.allocator.alloc_str(source_text);
302        // Unwrapping here, it should already be checked by [ReplaceGlobalDefinesConfig::new].
303        let mut expr = Parser::new(self.allocator, source_text, SourceType::default())
304            .parse_expression()
305            .unwrap();
306
307        UpdateReplacedExpression { ctx }.visit_expression(&mut expr);
308
309        expr
310    }
311
312    fn replace_identifier_defines(
313        &self,
314        expr: &mut Expression<'a>,
315        ctx: &mut TraverseCtx<'a>,
316    ) -> bool {
317        match expr {
318            Expression::Identifier(ident) => {
319                if let Some(new_expr) = self.replace_identifier_define_impl(ident, ctx) {
320                    *expr = new_expr;
321                    return true;
322                }
323            }
324            Expression::ThisExpression(_)
325                if self.config.0.identifier.has_this_expr_define
326                    && should_replace_this_expr(ctx.current_scope_flags()) =>
327            {
328                for (key, value) in &self.config.0.identifier.identifier_defines {
329                    if key.as_str() == "this" {
330                        let value = self.parse_value(value, ctx);
331                        *expr = value;
332
333                        return true;
334                    }
335                }
336            }
337            _ => {}
338        }
339        false
340    }
341
342    fn replace_identifier_define_impl(
343        &self,
344        ident: &oxc_allocator::Box<'_, IdentifierReference<'_>>,
345        ctx: &mut TraverseCtx<'a>,
346    ) -> Option<Expression<'a>> {
347        if let Some(symbol_id) = ident
348            .reference_id
349            .get()
350            .and_then(|reference_id| ctx.scoping().get_reference(reference_id).symbol_id())
351        {
352            // Ignore `declare const IS_PROD: boolean;`
353            if !ctx.scoping().symbol_flags(symbol_id).is_ambient() {
354                return None;
355            }
356        }
357        // This is a global variable, including ambient variants such as `declare const`.
358        for (key, value) in &self.config.0.identifier.identifier_defines {
359            if ident.name.as_str() == key {
360                let value = self.parse_value(value, ctx);
361                return Some(value);
362            }
363        }
364        None
365    }
366
367    fn replace_define_with_assignment_expr(
368        &self,
369        node: &mut AssignmentExpression<'a>,
370        ctx: &mut TraverseCtx<'a>,
371    ) -> bool {
372        let new_left = node
373            .left
374            .as_simple_assignment_target_mut()
375            .and_then(|item| match item {
376                SimpleAssignmentTarget::ComputedMemberExpression(computed_member_expr) => {
377                    self.replace_dot_computed_member_expr(computed_member_expr, ctx)
378                }
379                SimpleAssignmentTarget::StaticMemberExpression(member) => {
380                    self.replace_dot_static_member_expr(member, ctx)
381                }
382                SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => {
383                    self.replace_identifier_define_impl(ident, ctx)
384                }
385                _ => None,
386            })
387            .and_then(assignment_target_from_expr);
388        if let Some(new_left) = new_left {
389            node.left = new_left;
390            return true;
391        }
392        false
393    }
394
395    fn replace_dot_defines(&self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
396        match expr {
397            Expression::ChainExpression(chain) => {
398                let Some(new_expr) =
399                    chain.expression.as_member_expression_mut().and_then(|item| match item {
400                        MemberExpression::ComputedMemberExpression(computed_member_expr) => {
401                            self.replace_dot_computed_member_expr(computed_member_expr, ctx)
402                        }
403                        MemberExpression::StaticMemberExpression(member) => {
404                            self.replace_dot_static_member_expr(member, ctx)
405                        }
406                        MemberExpression::PrivateFieldExpression(_) => None,
407                    })
408                else {
409                    return false;
410                };
411                *expr = new_expr;
412                return true;
413            }
414            Expression::StaticMemberExpression(member) => {
415                if let Some(new_expr) = self.replace_dot_static_member_expr(member, ctx) {
416                    *expr = new_expr;
417                    return true;
418                }
419            }
420            Expression::ComputedMemberExpression(member) => {
421                if let Some(new_expr) = self.replace_dot_computed_member_expr(member, ctx) {
422                    *expr = new_expr;
423                    return true;
424                }
425            }
426            Expression::MetaProperty(meta_property) => {
427                if let Some(replacement) = &self.config.0.import_meta
428                    && meta_property.meta.name == "import"
429                    && meta_property.property.name == "meta"
430                {
431                    let value = self.parse_value(replacement, ctx);
432                    *expr = value;
433                    return true;
434                }
435            }
436            _ => {}
437        }
438        false
439    }
440
441    fn replace_dot_computed_member_expr(
442        &self,
443        member: &ComputedMemberExpression<'a>,
444        ctx: &mut TraverseCtx<'a>,
445    ) -> Option<Expression<'a>> {
446        for dot_define in &self.config.0.dot {
447            if Self::is_dot_define(
448                ctx,
449                dot_define,
450                DotDefineMemberExpression::ComputedMemberExpression(member),
451            ) {
452                let value = self.parse_value(&dot_define.value, ctx);
453                return Some(value);
454            }
455        }
456        // TODO: meta_property_define
457        None
458    }
459
460    fn replace_dot_static_member_expr(
461        &self,
462        member: &StaticMemberExpression<'a>,
463        ctx: &mut TraverseCtx<'a>,
464    ) -> Option<Expression<'a>> {
465        for dot_define in &self.config.0.dot {
466            if Self::is_dot_define(
467                ctx,
468                dot_define,
469                DotDefineMemberExpression::StaticMemberExpression(member),
470            ) {
471                let value = self.parse_value(&dot_define.value, ctx);
472                return Some(destructing_dot_define_optimizer(value, ctx));
473            }
474        }
475        for meta_property_define in &self.config.0.meta_property {
476            if Self::is_meta_property_define(meta_property_define, member) {
477                let value = self.parse_value(&meta_property_define.value, ctx);
478                return Some(destructing_dot_define_optimizer(value, ctx));
479            }
480        }
481        None
482    }
483
484    pub fn is_meta_property_define(
485        meta_define: &MetaPropertyDefine,
486        member: &StaticMemberExpression<'a>,
487    ) -> bool {
488        enum WildCardStatus {
489            None,
490            Pending,
491            Matched,
492        }
493        if meta_define.parts.is_empty() && meta_define.postfix_wildcard {
494            match &member.object {
495                Expression::MetaProperty(meta) => {
496                    return meta.meta.name == "import" && meta.property.name == "meta";
497                }
498                _ => return false,
499            }
500        }
501        debug_assert!(!meta_define.parts.is_empty());
502
503        let mut current_part_member_expression = Some(member);
504        let mut cur_part_name = &member.property.name;
505        let mut is_full_match = true;
506        let mut i = meta_define.parts.len() - 1;
507        let mut has_matched_part = false;
508        let mut wildcard_status = if meta_define.postfix_wildcard {
509            WildCardStatus::Pending
510        } else {
511            WildCardStatus::None
512        };
513        loop {
514            let part = &meta_define.parts[i];
515            let matched = cur_part_name.as_str() == part;
516            if matched {
517                has_matched_part = true;
518            } else {
519                is_full_match = false;
520                // Considering import.meta.env.*
521                // ```js
522                // import.meta.env.test // should matched
523                // import.res.meta.env // should not matched
524                // ```
525                // So we use has_matched_part to track if any part has matched.
526                // `None` means there is no postfix wildcard defined, so any part not matched should return false
527                // `Matched` means there is a postfix wildcard defined, and already matched a part, so any further
528                // not matched part should return false
529                if matches!(wildcard_status, WildCardStatus::None | WildCardStatus::Matched)
530                    || has_matched_part
531                {
532                    return false;
533                }
534                wildcard_status = WildCardStatus::Matched;
535            }
536
537            current_part_member_expression = if let Some(member) = current_part_member_expression {
538                match &member.object {
539                    Expression::StaticMemberExpression(member) => {
540                        cur_part_name = &member.property.name;
541                        Some(member)
542                    }
543                    Expression::MetaProperty(_) => {
544                        if meta_define.postfix_wildcard {
545                            // `import.meta.env` should not match `import.meta.env.*`
546                            return has_matched_part && !is_full_match;
547                        }
548                        return true;
549                    }
550                    Expression::Identifier(_) => {
551                        return false;
552                    }
553                    _ => None,
554                }
555            } else {
556                return false;
557            };
558
559            // Config `import.meta.env.* -> 'undefined'`
560            // Considering try replace `import.meta.env` to `undefined`, for the first loop the i is already
561            // 0, if it did not match part name and still reach here, that means
562            // current_part_member_expression is still something, and possible to match in the
563            // further loop
564            if i == 0 && matched {
565                break;
566            }
567
568            if matched {
569                i -= 1;
570            }
571        }
572
573        false
574    }
575
576    pub fn is_dot_define<'b>(
577        ctx: &TraverseCtx<'a>,
578        dot_define: &DotDefine,
579        member: DotDefineMemberExpression<'b, 'a>,
580    ) -> bool {
581        debug_assert!(dot_define.parts.len() > 1);
582        let should_replace_this_expr = should_replace_this_expr(ctx.current_scope_flags());
583        let Some(mut cur_part_name) = member.name() else {
584            return false;
585        };
586        let mut current_part_member_expression = Some(member);
587
588        for (i, part) in dot_define.parts.iter().enumerate().rev() {
589            if cur_part_name.as_str() != part {
590                return false;
591            }
592            if i == 0 {
593                break;
594            }
595
596            current_part_member_expression = if let Some(member) = current_part_member_expression {
597                match &member.object() {
598                    Expression::StaticMemberExpression(member) => {
599                        cur_part_name = &member.property.name;
600                        Some(DotDefineMemberExpression::StaticMemberExpression(member))
601                    }
602                    Expression::ComputedMemberExpression(computed_member) => {
603                        static_property_name_of_computed_expr(computed_member).map(|name| {
604                            cur_part_name = name;
605                            DotDefineMemberExpression::ComputedMemberExpression(computed_member)
606                        })
607                    }
608                    Expression::Identifier(ident) => {
609                        if !ident.is_global_reference(ctx.scoping()) {
610                            return false;
611                        }
612                        cur_part_name = &ident.name;
613                        None
614                    }
615                    Expression::ThisExpression(_) if should_replace_this_expr => {
616                        cur_part_name = &THIS_ATOM;
617                        None
618                    }
619                    Expression::MetaProperty(meta) => {
620                        // Handle import.meta
621                        // When we encounter a MetaProperty, we need to verify that the remaining
622                        // parts match ["import", "meta"]
623                        if meta.meta.name == "import" && meta.property.name == "meta" {
624                            // At this point, i is the current position we're checking
625                            // We need the next two parts (going backwards) to be "meta" then "import"
626                            // i.e., parts[i-1] == "meta" and parts[i-2] == "import"
627                            if i >= 2
628                                && dot_define.parts[i - 1].as_str() == "meta"
629                                && dot_define.parts[i - 2].as_str() == "import"
630                            {
631                                // Successfully matched import.meta at the expected position
632                                // Return true if we've consumed all parts (i == 2)
633                                return i == 2;
634                            }
635                        }
636                        None
637                    }
638                    _ => None,
639                }
640            } else {
641                return false;
642            };
643        }
644
645        current_part_member_expression.is_none()
646    }
647}
648
649#[derive(Debug, Clone, Copy)]
650pub enum DotDefineMemberExpression<'b, 'ast: 'b> {
651    StaticMemberExpression(&'b StaticMemberExpression<'ast>),
652    ComputedMemberExpression(&'b ComputedMemberExpression<'ast>),
653}
654
655impl<'b, 'a> DotDefineMemberExpression<'b, 'a> {
656    fn name(&self) -> Option<&'b Atom<'a>> {
657        match self {
658            DotDefineMemberExpression::StaticMemberExpression(expr) => Some(&expr.property.name),
659            DotDefineMemberExpression::ComputedMemberExpression(expr) => {
660                static_property_name_of_computed_expr(expr)
661            }
662        }
663    }
664
665    fn object(&self) -> &'b Expression<'a> {
666        match self {
667            DotDefineMemberExpression::StaticMemberExpression(expr) => &expr.object,
668            DotDefineMemberExpression::ComputedMemberExpression(expr) => &expr.object,
669        }
670    }
671}
672
673fn static_property_name_of_computed_expr<'b, 'a: 'b>(
674    expr: &'b ComputedMemberExpression<'a>,
675) -> Option<&'b Atom<'a>> {
676    match &expr.expression {
677        Expression::StringLiteral(lit) => Some(&lit.value),
678        Expression::TemplateLiteral(lit) if lit.expressions.is_empty() && lit.quasis.len() == 1 => {
679            Some(&lit.quasis[0].value.raw)
680        }
681        _ => None,
682    }
683}
684
685fn destructing_dot_define_optimizer<'ast>(
686    mut expr: Expression<'ast>,
687    ctx: &TraverseCtx<'ast>,
688) -> Expression<'ast> {
689    let Expression::ObjectExpression(obj) = &mut expr else { return expr };
690    let parent = ctx.parent();
691    let destruct_obj_pat = match parent {
692        Ancestor::VariableDeclaratorInit(declarator) => match &declarator.id().kind {
693            BindingPatternKind::ObjectPattern(pat) => pat,
694            _ => return expr,
695        },
696        _ => {
697            return expr;
698        }
699    };
700    let mut needed_keys = FxHashSet::default();
701    for prop in &destruct_obj_pat.properties {
702        match prop.key.name() {
703            Some(key) => {
704                needed_keys.insert(key);
705            }
706            // if there exists a none static key, we can't optimize
707            None => {
708                return expr;
709            }
710        }
711    }
712
713    // here we iterate the object properties twice
714    // for the first time we check if all the keys are static
715    // for the second time we only keep the needed keys
716    // Another way to do this is mutate the objectExpr only the fly,
717    // but need to save the checkpoint(to return the original Expr if there are any dynamic key exists) which is a memory clone,
718    // cpu is faster than memory allocation
719    let mut should_preserved_keys = Vec::with_capacity(obj.properties.len());
720    for prop in &obj.properties {
721        let v = match prop {
722            ObjectPropertyKind::ObjectProperty(prop) => {
723                // not static key just preserve it
724                if let Some(name) = prop.key.name() { needed_keys.contains(&name) } else { true }
725            }
726            // not static key
727            ObjectPropertyKind::SpreadProperty(_) => true,
728        };
729        should_preserved_keys.push(v);
730    }
731
732    // we could ensure `should_preserved_keys` has the same length as `obj.properties`
733    // the method copy from std doc https://doc.rust-lang.org/std/vec/struct.Vec.html#examples-26
734    let mut iter = should_preserved_keys.iter();
735    obj.properties.retain(|_| *iter.next().unwrap());
736    expr
737}
738
739const fn should_replace_this_expr(scope_flags: ScopeFlags) -> bool {
740    !scope_flags.contains(ScopeFlags::Function) || scope_flags.contains(ScopeFlags::Arrow)
741}
742
743fn assignment_target_from_expr(expr: Expression) -> Option<AssignmentTarget> {
744    match expr {
745        Expression::ComputedMemberExpression(expr) => {
746            Some(AssignmentTarget::ComputedMemberExpression(expr))
747        }
748        Expression::StaticMemberExpression(expr) => {
749            Some(AssignmentTarget::StaticMemberExpression(expr))
750        }
751        Expression::Identifier(ident) => Some(AssignmentTarget::AssignmentTargetIdentifier(ident)),
752        _ => None,
753    }
754}
755
756/// Update the replaced expression:
757/// * change spans to empty spans for sourcemap
758/// * assign reference id in current scope
759struct UpdateReplacedExpression<'a, 'b> {
760    ctx: &'b mut TraverseCtx<'a>,
761}
762
763impl VisitMut<'_> for UpdateReplacedExpression<'_, '_> {
764    fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'_>) {
765        let reference_id =
766            self.ctx.create_reference_in_current_scope(ident.name.as_str(), ReferenceFlags::Read);
767        ident.set_reference_id(reference_id);
768        walk_mut::walk_identifier_reference(self, ident);
769    }
770
771    fn visit_span(&mut self, span: &mut Span) {
772        *span = SPAN;
773    }
774}