Skip to content

Commit

Permalink
feat(minifier): add ReplaceGlobalDefinitions ast pass
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Jun 21, 2024
1 parent 58e54f4 commit d4418e4
Show file tree
Hide file tree
Showing 16 changed files with 205 additions and 41 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions crates/oxc_minifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ workspace = true
doctest = false

[dependencies]
oxc_allocator = { workspace = true }
oxc_span = { workspace = true }
oxc_ast = { workspace = true }
oxc_semantic = { workspace = true }
oxc_syntax = { workspace = true }
oxc_index = { workspace = true }
oxc_allocator = { workspace = true }
oxc_span = { workspace = true }
oxc_ast = { workspace = true }
oxc_semantic = { workspace = true }
oxc_syntax = { workspace = true }
oxc_index = { workspace = true }
oxc_parser = { workspace = true }
oxc_diagnostics = { workspace = true }

num-bigint = { workspace = true }
itertools = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod remove_dead_code;
mod remove_parens;
mod replace_global_defines;

pub use remove_dead_code::RemoveDeadCode;
pub use remove_parens::RemoveParens;
pub use replace_global_defines::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};
1 change: 0 additions & 1 deletion crates/oxc_minifier/src/ast_passes/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use oxc_span::SPAN;
/// Remove Dead Code from the AST.
///
/// Terser option: `dead_code: true`.
#[derive(Clone, Copy)]
pub struct RemoveDeadCode<'a> {
ast: AstBuilder<'a>,
}
Expand Down
13 changes: 4 additions & 9 deletions crates/oxc_minifier/src/ast_passes/remove_parens.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
use oxc_allocator::{Allocator, Vec};
use oxc_ast::{
ast::*,
visit::walk_mut::{walk_expression_mut, walk_statements_mut},
AstBuilder, VisitMut,
};
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};

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

fn strip_parenthesized_expression(self, expr: &mut Expression<'a>) {
fn strip_parenthesized_expression(&self, expr: &mut Expression<'a>) {
if let Expression::ParenthesizedExpression(paren_expr) = expr {
*expr = self.ast.move_expression(&mut paren_expr.expression);
self.strip_parenthesized_expression(expr);
Expand All @@ -31,11 +26,11 @@ impl<'a> RemoveParens<'a> {
impl<'a> VisitMut<'a> for RemoveParens<'a> {
fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
walk_statements_mut(self, stmts);
walk_mut::walk_statements_mut(self, stmts);
}

fn visit_expression(&mut self, expr: &mut Expression<'a>) {
self.strip_parenthesized_expression(expr);
walk_expression_mut(self, expr);
walk_mut::walk_expression_mut(self, expr);
}
}
104 changes: 104 additions & 0 deletions crates/oxc_minifier/src/ast_passes/replace_global_defines.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::sync::Arc;

use oxc_allocator::Allocator;
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};
use oxc_diagnostics::OxcDiagnostic;
use oxc_parser::Parser;
use oxc_span::SourceType;
use oxc_syntax::identifier::is_identifier_name;

/// Configuration for [ReplaceGlobalDefines].
///
/// Due to the usage of an arena allocator, the constructor will parse once for grammatical errors,
/// and does not save the constructed expression.
///
/// The data is stored in an `Arc` so this can be shared across threads.
#[derive(Debug, Clone)]
pub struct ReplaceGlobalDefinesConfig(Arc<ReplaceGlobalDefinesConfigImpl>);

#[derive(Debug)]
struct ReplaceGlobalDefinesConfigImpl {
identifier_defines: Vec<(/* key */ String, /* value */ String)>,
// TODO: dot defines
}

impl ReplaceGlobalDefinesConfig {
/// # Errors
///
/// * key is not an identifier
/// * value has a syntax error
pub fn new<S: AsRef<str>>(defines: &[(S, S)]) -> Result<Self, Vec<OxcDiagnostic>> {
let allocator = Allocator::default();
let mut identifier_defines = vec![];
for (key, value) in defines {
let key = key.as_ref();
let value = value.as_ref();
Self::check_key(key)?;
Self::check_value(&allocator, value)?;
identifier_defines.push((key.to_string(), value.to_string()));
}
Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { identifier_defines })))
}

fn check_key(key: &str) -> Result<(), Vec<OxcDiagnostic>> {
if !is_identifier_name(key) {
return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]);
}
Ok(())
}

fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec<OxcDiagnostic>> {
Parser::new(allocator, source_text, SourceType::default()).parse_expression()?;
Ok(())
}
}

/// Replace Global Defines.
///
/// References:
///
/// * <https://esbuild.github.io/api/#define>
/// * <https://github.com/terser/terser?tab=readme-ov-file#conditional-compilation>
pub struct ReplaceGlobalDefines<'a> {
ast: AstBuilder<'a>,
config: ReplaceGlobalDefinesConfig,
}

impl<'a> ReplaceGlobalDefines<'a> {
pub fn new(allocator: &'a Allocator, config: ReplaceGlobalDefinesConfig) -> Self {
Self { ast: AstBuilder::new(allocator), config }
}

pub fn build(&mut self, program: &mut Program<'a>) {
self.visit_program(program);
}

// Construct a new expression because we don't have ast clone right now.
fn parse_value(&self, source_text: &str) -> Expression<'a> {
// Allocate the string lazily because replacement happens rarely.
let source_text = self.ast.allocator.alloc(source_text.to_string());
// Unwrapping here, it should already be checked by [ReplaceGlobalDefinesConfig::new].
Parser::new(self.ast.allocator, source_text, SourceType::default())
.parse_expression()
.unwrap()
}

fn replace_identifier_defines(&self, expr: &mut Expression<'a>) {
for (key, value) in &self.config.0.identifier_defines {
if let Expression::Identifier(ident) = expr {
if ident.name.as_str() == key {
let value = self.parse_value(value);
*expr = value;
break;
}
}
}
}
}

impl<'a> VisitMut<'a> for ReplaceGlobalDefines<'a> {
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
self.replace_identifier_defines(expr);
walk_mut::walk_expression_mut(self, expr);
}
}
2 changes: 1 addition & 1 deletion crates/oxc_minifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use oxc_allocator::Allocator;
use oxc_ast::ast::Program;

pub use crate::{
ast_passes::{RemoveDeadCode, RemoveParens},
ast_passes::{RemoveDeadCode, RemoveParens, ReplaceGlobalDefines, ReplaceGlobalDefinesConfig},
compressor::{CompressOptions, Compressor},
mangler::ManglerBuilder,
};
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_minifier/tests/oxc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ mod code_removal;
mod folding;
mod precedence;
mod remove_dead_code;
mod replace_global_defines;
23 changes: 23 additions & 0 deletions crates/oxc_minifier/tests/oxc/replace_global_defines.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use oxc_allocator::Allocator;
use oxc_codegen::WhitespaceRemover;
use oxc_minifier::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};
use oxc_parser::Parser;
use oxc_span::SourceType;

pub(crate) fn test(source_text: &str, expected: &str, config: ReplaceGlobalDefinesConfig) {
let minified = {
let source_type = SourceType::default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);
ReplaceGlobalDefines::new(&allocator, config).build(program);
WhitespaceRemover::new().build(program).source_text
};
assert_eq!(minified, expected, "for source {source_text}");
}

#[test]
fn replace_global_definitions() {
let config = ReplaceGlobalDefinesConfig::new(&[("id", "text"), ("str", "'text'")]).unwrap();
test("id, str", "text,'text'", config);
}
2 changes: 1 addition & 1 deletion crates/oxc_parser/src/js/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ impl<'a> ParserImpl<'a> {
let value = if self.eat(Kind::Eq) {
// let current_flags = self.scope.current_flags();
// self.scope.set_current_flags(self.scope.current_flags());
let expr = self.parse_expression()?;
let expr = self.parse_expr()?;
// self.scope.set_current_flags(current_flags);
Some(expr)
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_parser/src/js/declaration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl<'a> ParserImpl<'a> {
self.parse_expression_statement(span, expr)
// let.a = 1, let()[a] = 1
} else if matches!(peeked, Kind::Dot | Kind::LParen) {
let expr = self.parse_expression()?;
let expr = self.parse_expr()?;
Ok(self.ast.expression_statement(self.end_span(span), expr))
// single statement let declaration: while (0) let
} else if (stmt_ctx.is_single_statement() && peeked != Kind::LBrack)
Expand Down
15 changes: 6 additions & 9 deletions crates/oxc_parser/src/js/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ use crate::{
impl<'a> ParserImpl<'a> {
pub(crate) fn parse_paren_expression(&mut self) -> Result<Expression<'a>> {
self.expect(Kind::LParen)?;
let expression = self.parse_expression()?;
let expression = self.parse_expr()?;
self.expect(Kind::RParen)?;
Ok(expression)
}

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

let has_decorator = self.ctx.has_decorator();
Expand Down Expand Up @@ -386,7 +386,7 @@ impl<'a> ParserImpl<'a> {
Kind::TemplateHead => {
quasis.push(self.parse_template_element(tagged));
// TemplateHead Expression[+In, ?Yield, ?Await]
let expr = self.context(Context::In, Context::empty(), Self::parse_expression)?;
let expr = self.context(Context::In, Context::empty(), Self::parse_expr)?;
expressions.push(expr);
self.re_lex_template_substitution_tail();
loop {
Expand All @@ -401,11 +401,8 @@ impl<'a> ParserImpl<'a> {
}
_ => {
// TemplateMiddle Expression[+In, ?Yield, ?Await]
let expr = self.context(
Context::In,
Context::empty(),
Self::parse_expression,
)?;
let expr =
self.context(Context::In, Context::empty(), Self::parse_expr)?;
expressions.push(expr);
self.re_lex_template_substitution_tail();
}
Expand Down Expand Up @@ -652,7 +649,7 @@ impl<'a> ParserImpl<'a> {
optional: bool,
) -> Result<Expression<'a>> {
self.bump_any(); // advance `[`
let property = self.context(Context::In, Context::empty(), Self::parse_expression)?;
let property = self.context(Context::In, Context::empty(), Self::parse_expr)?;
self.expect(Kind::RBrack)?;
Ok(self.ast.computed_member_expression(self.end_span(lhs_span), lhs, property, optional))
}
Expand Down
16 changes: 8 additions & 8 deletions crates/oxc_parser/src/js/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ impl<'a> ParserImpl<'a> {

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

let init_expression =
self.context(Context::empty(), Context::In, ParserImpl::parse_expression)?;
self.context(Context::empty(), Context::In, ParserImpl::parse_expr)?;

// for (a.b in ...), for ([a] in ..), for ({a} in ..)
if self.at(Kind::In) || self.at(Kind::Of) {
Expand Down Expand Up @@ -358,15 +358,15 @@ impl<'a> ParserImpl<'a> {
) -> Result<Statement<'a>> {
self.expect(Kind::Semicolon)?;
let test = if !self.at(Kind::Semicolon) && !self.at(Kind::RParen) {
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?)
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?)
} else {
None
};
self.expect(Kind::Semicolon)?;
let update = if self.at(Kind::RParen) {
None
} else {
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?)
Some(self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?)
};
self.expect(Kind::RParen)?;
if r#await {
Expand All @@ -385,7 +385,7 @@ impl<'a> ParserImpl<'a> {
let is_for_in = self.at(Kind::In);
self.bump_any(); // bump `in` or `of`
let right = if is_for_in {
self.parse_expression()
self.parse_expr()
} else {
self.parse_assignment_expression_or_higher()
}?;
Expand Down Expand Up @@ -432,7 +432,7 @@ impl<'a> ParserImpl<'a> {
let argument = if self.eat(Kind::Semicolon) || self.can_insert_semicolon() {
None
} else {
let expr = self.context(Context::In, Context::empty(), ParserImpl::parse_expression)?;
let expr = self.context(Context::In, Context::empty(), ParserImpl::parse_expr)?;
self.asi()?;
Some(expr)
};
Expand Down Expand Up @@ -477,7 +477,7 @@ impl<'a> ParserImpl<'a> {
}
Kind::Case => {
self.bump_any();
let expression = self.parse_expression()?;
let expression = self.parse_expr()?;
Some(expression)
}
_ => return Err(self.unexpected()),
Expand All @@ -502,7 +502,7 @@ impl<'a> ParserImpl<'a> {
self.cur_token().span(),
));
}
let argument = self.parse_expression()?;
let argument = self.parse_expr()?;
self.asi()?;
Ok(self.ast.throw_statement(self.end_span(span), argument))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_parser/src/jsx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ impl<'a> ParserImpl<'a> {

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

0 comments on commit d4418e4

Please sign in to comment.