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#[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<(CompactStr, CompactStr)>,
28 has_this_expr_define: bool,
30}
31#[derive(Debug)]
32struct ReplaceGlobalDefinesConfigImpl {
33 identifier: IdentifierDefine,
34 dot: Vec<DotDefine>,
35 meta_property: Vec<MetaPropertyDefine>,
36 import_meta: Option<CompactStr>,
41}
42
43#[derive(Debug)]
44pub struct DotDefine {
45 pub parts: Vec<CompactStr>,
47 pub value: CompactStr,
48}
49
50#[derive(Debug)]
51pub struct MetaPropertyDefine {
52 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 ImportMetaWithParts { parts: Vec<CompactStr>, postfix_wildcard: bool },
75 ImportMeta(bool),
77}
78
79impl ReplaceGlobalDefinesConfig {
80 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 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 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 } 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
210pub struct ReplaceGlobalDefines<'a> {
218 allocator: &'a Allocator,
219 config: ReplaceGlobalDefinesConfig,
220 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 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 fn parse_value(&self, source_text: &str) -> Expression<'a> {
289 let source_text = self.allocator.alloc_str(source_text);
291 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 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 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 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 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 None => {
651 return expr;
652 }
653 }
654 }
655
656 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 if let Some(name) = prop.key.name() { needed_keys.contains(&name) } else { true }
668 }
669 ObjectPropertyKind::SpreadProperty(_) => true,
671 };
672 should_preserved_keys.push(v);
673 }
674
675 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}