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#[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<(CompactStr, CompactStr)>,
31 has_this_expr_define: bool,
33}
34#[derive(Debug)]
35struct ReplaceGlobalDefinesConfigImpl {
36 identifier: IdentifierDefine,
37 dot: Vec<DotDefine>,
38 meta_property: Vec<MetaPropertyDefine>,
39 import_meta: Option<CompactStr>,
44}
45
46#[derive(Debug)]
47pub struct DotDefine {
48 pub parts: Vec<CompactStr>,
50 pub value: CompactStr,
51}
52
53#[derive(Debug)]
54pub struct MetaPropertyDefine {
55 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 ImportMetaWithParts { parts: Vec<CompactStr>, postfix_wildcard: bool },
78 ImportMeta(bool),
80}
81
82impl ReplaceGlobalDefinesConfig {
83 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 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 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 } 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
214pub struct ReplaceGlobalDefines<'a> {
222 allocator: &'a Allocator,
223 config: ReplaceGlobalDefinesConfig,
224 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 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 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 fn parse_value(&self, source_text: &str, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
300 let source_text = self.allocator.alloc_str(source_text);
302 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 if !ctx.scoping().symbol_flags(symbol_id).is_ambient() {
354 return None;
355 }
356 }
357 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 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 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 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 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 if meta.meta.name == "import" && meta.property.name == "meta" {
624 if i >= 2
628 && dot_define.parts[i - 1].as_str() == "meta"
629 && dot_define.parts[i - 2].as_str() == "import"
630 {
631 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 None => {
708 return expr;
709 }
710 }
711 }
712
713 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 if let Some(name) = prop.key.name() { needed_keys.contains(&name) } else { true }
725 }
726 ObjectPropertyKind::SpreadProperty(_) => true,
728 };
729 should_preserved_keys.push(v);
730 }
731
732 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
756struct 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}