oxc_transformer_plugins/
replace_global_defines.rs

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