Skip to content

Commit dd540c8

Browse files
committed
feat(minifier): add skeleton for ReplaceGlobalDefines ast pass (#3803)
1 parent 58e54f4 commit dd540c8

16 files changed

+205
-41
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_minifier/Cargo.toml

+8-6
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ workspace = true
2020
doctest = false
2121

2222
[dependencies]
23-
oxc_allocator = { workspace = true }
24-
oxc_span = { workspace = true }
25-
oxc_ast = { workspace = true }
26-
oxc_semantic = { workspace = true }
27-
oxc_syntax = { workspace = true }
28-
oxc_index = { workspace = true }
23+
oxc_allocator = { workspace = true }
24+
oxc_span = { workspace = true }
25+
oxc_ast = { workspace = true }
26+
oxc_semantic = { workspace = true }
27+
oxc_syntax = { workspace = true }
28+
oxc_index = { workspace = true }
29+
oxc_parser = { workspace = true }
30+
oxc_diagnostics = { workspace = true }
2931

3032
num-bigint = { workspace = true }
3133
itertools = { workspace = true }

crates/oxc_minifier/src/ast_passes/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
mod remove_dead_code;
44
mod remove_parens;
5+
mod replace_global_defines;
56

67
pub use remove_dead_code::RemoveDeadCode;
78
pub use remove_parens::RemoveParens;
9+
pub use replace_global_defines::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};

crates/oxc_minifier/src/ast_passes/remove_dead_code.rs

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use oxc_span::SPAN;
55
/// Remove Dead Code from the AST.
66
///
77
/// Terser option: `dead_code: true`.
8-
#[derive(Clone, Copy)]
98
pub struct RemoveDeadCode<'a> {
109
ast: AstBuilder<'a>,
1110
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
use oxc_allocator::{Allocator, Vec};
2-
use oxc_ast::{
3-
ast::*,
4-
visit::walk_mut::{walk_expression_mut, walk_statements_mut},
5-
AstBuilder, VisitMut,
6-
};
2+
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};
73

84
/// Remove Parenthesized Expression from the AST.
9-
#[derive(Clone, Copy)]
105
pub struct RemoveParens<'a> {
116
ast: AstBuilder<'a>,
127
}
@@ -20,7 +15,7 @@ impl<'a> RemoveParens<'a> {
2015
self.visit_program(program);
2116
}
2217

23-
fn strip_parenthesized_expression(self, expr: &mut Expression<'a>) {
18+
fn strip_parenthesized_expression(&self, expr: &mut Expression<'a>) {
2419
if let Expression::ParenthesizedExpression(paren_expr) = expr {
2520
*expr = self.ast.move_expression(&mut paren_expr.expression);
2621
self.strip_parenthesized_expression(expr);
@@ -31,11 +26,11 @@ impl<'a> RemoveParens<'a> {
3126
impl<'a> VisitMut<'a> for RemoveParens<'a> {
3227
fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
3328
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
34-
walk_statements_mut(self, stmts);
29+
walk_mut::walk_statements_mut(self, stmts);
3530
}
3631

3732
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
3833
self.strip_parenthesized_expression(expr);
39-
walk_expression_mut(self, expr);
34+
walk_mut::walk_expression_mut(self, expr);
4035
}
4136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::sync::Arc;
2+
3+
use oxc_allocator::Allocator;
4+
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_parser::Parser;
7+
use oxc_span::SourceType;
8+
use oxc_syntax::identifier::is_identifier_name;
9+
10+
/// Configuration for [ReplaceGlobalDefines].
11+
///
12+
/// Due to the usage of an arena allocator, the constructor will parse once for grammatical errors,
13+
/// and does not save the constructed expression.
14+
///
15+
/// The data is stored in an `Arc` so this can be shared across threads.
16+
#[derive(Debug, Clone)]
17+
pub struct ReplaceGlobalDefinesConfig(Arc<ReplaceGlobalDefinesConfigImpl>);
18+
19+
#[derive(Debug)]
20+
struct ReplaceGlobalDefinesConfigImpl {
21+
identifier_defines: Vec<(/* key */ String, /* value */ String)>,
22+
// TODO: dot defines
23+
}
24+
25+
impl ReplaceGlobalDefinesConfig {
26+
/// # Errors
27+
///
28+
/// * key is not an identifier
29+
/// * value has a syntax error
30+
pub fn new<S: AsRef<str>>(defines: &[(S, S)]) -> Result<Self, Vec<OxcDiagnostic>> {
31+
let allocator = Allocator::default();
32+
let mut identifier_defines = vec![];
33+
for (key, value) in defines {
34+
let key = key.as_ref();
35+
let value = value.as_ref();
36+
Self::check_key(key)?;
37+
Self::check_value(&allocator, value)?;
38+
identifier_defines.push((key.to_string(), value.to_string()));
39+
}
40+
Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { identifier_defines })))
41+
}
42+
43+
fn check_key(key: &str) -> Result<(), Vec<OxcDiagnostic>> {
44+
if !is_identifier_name(key) {
45+
return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]);
46+
}
47+
Ok(())
48+
}
49+
50+
fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec<OxcDiagnostic>> {
51+
Parser::new(allocator, source_text, SourceType::default()).parse_expression()?;
52+
Ok(())
53+
}
54+
}
55+
56+
/// Replace Global Defines.
57+
///
58+
/// References:
59+
///
60+
/// * <https://esbuild.github.io/api/#define>
61+
/// * <https://github.com/terser/terser?tab=readme-ov-file#conditional-compilation>
62+
pub struct ReplaceGlobalDefines<'a> {
63+
ast: AstBuilder<'a>,
64+
config: ReplaceGlobalDefinesConfig,
65+
}
66+
67+
impl<'a> ReplaceGlobalDefines<'a> {
68+
pub fn new(allocator: &'a Allocator, config: ReplaceGlobalDefinesConfig) -> Self {
69+
Self { ast: AstBuilder::new(allocator), config }
70+
}
71+
72+
pub fn build(&mut self, program: &mut Program<'a>) {
73+
self.visit_program(program);
74+
}
75+
76+
// Construct a new expression because we don't have ast clone right now.
77+
fn parse_value(&self, source_text: &str) -> Expression<'a> {
78+
// Allocate the string lazily because replacement happens rarely.
79+
let source_text = self.ast.allocator.alloc(source_text.to_string());
80+
// Unwrapping here, it should already be checked by [ReplaceGlobalDefinesConfig::new].
81+
Parser::new(self.ast.allocator, source_text, SourceType::default())
82+
.parse_expression()
83+
.unwrap()
84+
}
85+
86+
fn replace_identifier_defines(&self, expr: &mut Expression<'a>) {
87+
for (key, value) in &self.config.0.identifier_defines {
88+
if let Expression::Identifier(ident) = expr {
89+
if ident.name.as_str() == key {
90+
let value = self.parse_value(value);
91+
*expr = value;
92+
break;
93+
}
94+
}
95+
}
96+
}
97+
}
98+
99+
impl<'a> VisitMut<'a> for ReplaceGlobalDefines<'a> {
100+
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
101+
self.replace_identifier_defines(expr);
102+
walk_mut::walk_expression_mut(self, expr);
103+
}
104+
}

crates/oxc_minifier/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use oxc_allocator::Allocator;
88
use oxc_ast::ast::Program;
99

1010
pub use crate::{
11-
ast_passes::{RemoveDeadCode, RemoveParens},
11+
ast_passes::{RemoveDeadCode, RemoveParens, ReplaceGlobalDefines, ReplaceGlobalDefinesConfig},
1212
compressor::{CompressOptions, Compressor},
1313
mangler::ManglerBuilder,
1414
};

crates/oxc_minifier/tests/oxc/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ mod code_removal;
22
mod folding;
33
mod precedence;
44
mod remove_dead_code;
5+
mod replace_global_defines;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use oxc_allocator::Allocator;
2+
use oxc_codegen::WhitespaceRemover;
3+
use oxc_minifier::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};
4+
use oxc_parser::Parser;
5+
use oxc_span::SourceType;
6+
7+
pub(crate) fn test(source_text: &str, expected: &str, config: ReplaceGlobalDefinesConfig) {
8+
let minified = {
9+
let source_type = SourceType::default();
10+
let allocator = Allocator::default();
11+
let ret = Parser::new(&allocator, source_text, source_type).parse();
12+
let program = allocator.alloc(ret.program);
13+
ReplaceGlobalDefines::new(&allocator, config).build(program);
14+
WhitespaceRemover::new().build(program).source_text
15+
};
16+
assert_eq!(minified, expected, "for source {source_text}");
17+
}
18+
19+
#[test]
20+
fn replace_global_definitions() {
21+
let config = ReplaceGlobalDefinesConfig::new(&[("id", "text"), ("str", "'text'")]).unwrap();
22+
test("id, str", "text,'text'", config);
23+
}

crates/oxc_parser/src/js/class.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ impl<'a> ParserImpl<'a> {
417417
let value = if self.eat(Kind::Eq) {
418418
// let current_flags = self.scope.current_flags();
419419
// self.scope.set_current_flags(self.scope.current_flags());
420-
let expr = self.parse_expression()?;
420+
let expr = self.parse_expr()?;
421421
// self.scope.set_current_flags(current_flags);
422422
Some(expr)
423423
} else {

crates/oxc_parser/src/js/declaration.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ impl<'a> ParserImpl<'a> {
1616
self.parse_expression_statement(span, expr)
1717
// let.a = 1, let()[a] = 1
1818
} else if matches!(peeked, Kind::Dot | Kind::LParen) {
19-
let expr = self.parse_expression()?;
19+
let expr = self.parse_expr()?;
2020
Ok(self.ast.expression_statement(self.end_span(span), expr))
2121
// single statement let declaration: while (0) let
2222
} else if (stmt_ctx.is_single_statement() && peeked != Kind::LBrack)

crates/oxc_parser/src/js/expression.rs

+6-9
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ use crate::{
2828
impl<'a> ParserImpl<'a> {
2929
pub(crate) fn parse_paren_expression(&mut self) -> Result<Expression<'a>> {
3030
self.expect(Kind::LParen)?;
31-
let expression = self.parse_expression()?;
31+
let expression = self.parse_expr()?;
3232
self.expect(Kind::RParen)?;
3333
Ok(expression)
3434
}
3535

3636
/// Section [Expression](https://tc39.es/ecma262/#sec-ecmascript-language-expressions)
37-
pub(crate) fn parse_expression(&mut self) -> Result<Expression<'a>> {
37+
pub(crate) fn parse_expr(&mut self) -> Result<Expression<'a>> {
3838
let span = self.start_span();
3939

4040
let has_decorator = self.ctx.has_decorator();
@@ -386,7 +386,7 @@ impl<'a> ParserImpl<'a> {
386386
Kind::TemplateHead => {
387387
quasis.push(self.parse_template_element(tagged));
388388
// TemplateHead Expression[+In, ?Yield, ?Await]
389-
let expr = self.context(Context::In, Context::empty(), Self::parse_expression)?;
389+
let expr = self.context(Context::In, Context::empty(), Self::parse_expr)?;
390390
expressions.push(expr);
391391
self.re_lex_template_substitution_tail();
392392
loop {
@@ -401,11 +401,8 @@ impl<'a> ParserImpl<'a> {
401401
}
402402
_ => {
403403
// TemplateMiddle Expression[+In, ?Yield, ?Await]
404-
let expr = self.context(
405-
Context::In,
406-
Context::empty(),
407-
Self::parse_expression,
408-
)?;
404+
let expr =
405+
self.context(Context::In, Context::empty(), Self::parse_expr)?;
409406
expressions.push(expr);
410407
self.re_lex_template_substitution_tail();
411408
}
@@ -652,7 +649,7 @@ impl<'a> ParserImpl<'a> {
652649
optional: bool,
653650
) -> Result<Expression<'a>> {
654651
self.bump_any(); // advance `[`
655-
let property = self.context(Context::In, Context::empty(), Self::parse_expression)?;
652+
let property = self.context(Context::In, Context::empty(), Self::parse_expr)?;
656653
self.expect(Kind::RBrack)?;
657654
Ok(self.ast.computed_member_expression(self.end_span(lhs_span), lhs, property, optional))
658655
}

crates/oxc_parser/src/js/statement.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ impl<'a> ParserImpl<'a> {
142142

143143
fn parse_expression_or_labeled_statement(&mut self) -> Result<Statement<'a>> {
144144
let span = self.start_span();
145-
let expr = self.parse_expression()?;
145+
let expr = self.parse_expr()?;
146146
if let Expression::Identifier(ident) = &expr {
147147
// Section 14.13 Labelled Statement
148148
// Avoids lookahead for a labeled statement, which is on a hot path
@@ -282,7 +282,7 @@ impl<'a> ParserImpl<'a> {
282282
}
283283

284284
let init_expression =
285-
self.context(Context::empty(), Context::In, ParserImpl::parse_expression)?;
285+
self.context(Context::empty(), Context::In, ParserImpl::parse_expr)?;
286286

287287
// for (a.b in ...), for ([a] in ..), for ({a} in ..)
288288
if self.at(Kind::In) || self.at(Kind::Of) {
@@ -358,15 +358,15 @@ impl<'a> ParserImpl<'a> {
358358
) -> Result<Statement<'a>> {
359359
self.expect(Kind::Semicolon)?;
360360
let test = if !self.at(Kind::Semicolon) && !self.at(Kind::RParen) {
361-
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?)
361+
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?)
362362
} else {
363363
None
364364
};
365365
self.expect(Kind::Semicolon)?;
366366
let update = if self.at(Kind::RParen) {
367367
None
368368
} else {
369-
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?)
369+
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?)
370370
};
371371
self.expect(Kind::RParen)?;
372372
if r#await {
@@ -385,7 +385,7 @@ impl<'a> ParserImpl<'a> {
385385
let is_for_in = self.at(Kind::In);
386386
self.bump_any(); // bump `in` or `of`
387387
let right = if is_for_in {
388-
self.parse_expression()
388+
self.parse_expr()
389389
} else {
390390
self.parse_assignment_expression_or_higher()
391391
}?;
@@ -432,7 +432,7 @@ impl<'a> ParserImpl<'a> {
432432
let argument = if self.eat(Kind::Semicolon) || self.can_insert_semicolon() {
433433
None
434434
} else {
435-
let expr = self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?;
435+
let expr = self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?;
436436
self.asi()?;
437437
Some(expr)
438438
};
@@ -477,7 +477,7 @@ impl<'a> ParserImpl<'a> {
477477
}
478478
Kind::Case => {
479479
self.bump_any();
480-
let expression = self.parse_expression()?;
480+
let expression = self.parse_expr()?;
481481
Some(expression)
482482
}
483483
_ => return Err(self.unexpected()),
@@ -502,7 +502,7 @@ impl<'a> ParserImpl<'a> {
502502
self.cur_token().span(),
503503
));
504504
}
505-
let argument = self.parse_expression()?;
505+
let argument = self.parse_expr()?;
506506
self.asi()?;
507507
Ok(self.ast.throw_statement(self.end_span(span), argument))
508508
}

crates/oxc_parser/src/jsx/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl<'a> ParserImpl<'a> {
265265

266266
fn parse_jsx_assignment_expression(&mut self) -> Result<Expression<'a>> {
267267
self.context(Context::default().and_await(self.ctx.has_await()), self.ctx, |p| {
268-
let expr = p.parse_expression();
268+
let expr = p.parse_expr();
269269
if let Ok(Expression::SequenceExpression(seq)) = &expr {
270270
return Err(diagnostics::jsx_expressions_may_not_use_the_comma_operator(seq.span));
271271
}

0 commit comments

Comments
 (0)