Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minifier): add skeleton for ReplaceGlobalDefines ast pass #3803

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading