diff --git a/Cargo.lock b/Cargo.lock index b25828cc4def2..141ad9d860326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,7 @@ dependencies = [ "oxc_allocator", "oxc_ast", "oxc_codegen", + "oxc_diagnostics", "oxc_index", "oxc_parser", "oxc_semantic", diff --git a/crates/oxc_minifier/Cargo.toml b/crates/oxc_minifier/Cargo.toml index 9fd1094b600d3..ba8d93649331e 100644 --- a/crates/oxc_minifier/Cargo.toml +++ b/crates/oxc_minifier/Cargo.toml @@ -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 } diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 892167151f248..72767fdd3fd2f 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -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}; diff --git a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs index b7147d177fbbc..3bfbdbd9e5434 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs @@ -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>, } diff --git a/crates/oxc_minifier/src/ast_passes/remove_parens.rs b/crates/oxc_minifier/src/ast_passes/remove_parens.rs index 09a36289f380e..3f59a63a2f50a 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_parens.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_parens.rs @@ -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>, } @@ -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); @@ -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); } } diff --git a/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs b/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs new file mode 100644 index 0000000000000..ab8a197505cfa --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs @@ -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); + +#[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>(defines: &[(S, S)]) -> Result> { + 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> { + 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> { + Parser::new(allocator, source_text, SourceType::default()).parse_expression()?; + Ok(()) + } +} + +/// Replace Global Defines. +/// +/// References: +/// +/// * +/// * +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); + } +} diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index b6206ea04d7e8..d644212e11077 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -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, }; diff --git a/crates/oxc_minifier/tests/oxc/mod.rs b/crates/oxc_minifier/tests/oxc/mod.rs index f148ba3a16805..ecb9973b78d86 100644 --- a/crates/oxc_minifier/tests/oxc/mod.rs +++ b/crates/oxc_minifier/tests/oxc/mod.rs @@ -2,3 +2,4 @@ mod code_removal; mod folding; mod precedence; mod remove_dead_code; +mod replace_global_defines; diff --git a/crates/oxc_minifier/tests/oxc/replace_global_defines.rs b/crates/oxc_minifier/tests/oxc/replace_global_defines.rs new file mode 100644 index 0000000000000..0c390a1adc710 --- /dev/null +++ b/crates/oxc_minifier/tests/oxc/replace_global_defines.rs @@ -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); +} diff --git a/crates/oxc_parser/src/js/class.rs b/crates/oxc_parser/src/js/class.rs index dd40975e6401c..b83638d151aa5 100644 --- a/crates/oxc_parser/src/js/class.rs +++ b/crates/oxc_parser/src/js/class.rs @@ -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 { diff --git a/crates/oxc_parser/src/js/declaration.rs b/crates/oxc_parser/src/js/declaration.rs index 382058ad2df59..d2f0d259c8d95 100644 --- a/crates/oxc_parser/src/js/declaration.rs +++ b/crates/oxc_parser/src/js/declaration.rs @@ -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) diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index 6228b82f07ced..00e596601b749 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -28,13 +28,13 @@ use crate::{ impl<'a> ParserImpl<'a> { pub(crate) fn parse_paren_expression(&mut self) -> Result> { 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> { + pub(crate) fn parse_expr(&mut self) -> Result> { let span = self.start_span(); let has_decorator = self.ctx.has_decorator(); @@ -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 { @@ -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(); } @@ -652,7 +649,7 @@ impl<'a> ParserImpl<'a> { optional: bool, ) -> Result> { 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)) } diff --git a/crates/oxc_parser/src/js/statement.rs b/crates/oxc_parser/src/js/statement.rs index 0dfbd8461732d..89653ae7a6dca 100644 --- a/crates/oxc_parser/src/js/statement.rs +++ b/crates/oxc_parser/src/js/statement.rs @@ -142,7 +142,7 @@ impl<'a> ParserImpl<'a> { fn parse_expression_or_labeled_statement(&mut self) -> Result> { 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 @@ -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) { @@ -358,7 +358,7 @@ impl<'a> ParserImpl<'a> { ) -> Result> { 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 }; @@ -366,7 +366,7 @@ impl<'a> ParserImpl<'a> { 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 { @@ -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() }?; @@ -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) }; @@ -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()), @@ -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)) } diff --git a/crates/oxc_parser/src/jsx/mod.rs b/crates/oxc_parser/src/jsx/mod.rs index e9535bb0c3d50..e4a495984ebae 100644 --- a/crates/oxc_parser/src/jsx/mod.rs +++ b/crates/oxc_parser/src/jsx/mod.rs @@ -265,7 +265,7 @@ impl<'a> ParserImpl<'a> { fn parse_jsx_assignment_expression(&mut self) -> Result> { 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)); } diff --git a/crates/oxc_parser/src/lib.rs b/crates/oxc_parser/src/lib.rs index a6adf218982e7..cbe6cc7329cda 100644 --- a/crates/oxc_parser/src/lib.rs +++ b/crates/oxc_parser/src/lib.rs @@ -81,7 +81,10 @@ pub mod lexer; use context::{Context, StatementContext}; use oxc_allocator::Allocator; -use oxc_ast::{ast::Program, AstBuilder, Trivias}; +use oxc_ast::{ + ast::{Expression, Program}, + AstBuilder, Trivias, +}; use oxc_diagnostics::{OxcDiagnostic, Result}; use oxc_span::{ModuleKind, SourceType, Span}; @@ -227,6 +230,23 @@ mod parser_parse { ); parser.parse() } + + /// Parse `Expression` + /// + /// # Errors + /// + /// * Syntax Error + pub fn parse_expression(self) -> std::result::Result, Vec> { + let unique = UniquePromise::new(); + let parser = ParserImpl::new( + self.allocator, + self.source_text, + self.source_type, + self.options, + unique, + ); + parser.parse_expression() + } } } use parser_parse::UniquePromise; @@ -333,6 +353,17 @@ impl<'a> ParserImpl<'a> { ParserReturn { program, errors, trivias, panicked } } + pub fn parse_expression(mut self) -> std::result::Result, Vec> { + // initialize cur_token and prev_token by moving onto the first token + self.bump_any(); + let expr = self.parse_expr().map_err(|diagnostic| vec![diagnostic])?; + let errors = self.lexer.errors.into_iter().chain(self.errors).collect::>(); + if !errors.is_empty() { + return Err(errors); + } + Ok(expr) + } + #[allow(clippy::cast_possible_truncation)] fn parse_program(&mut self) -> Result> { // initialize cur_token and prev_token by moving onto the first token @@ -407,12 +438,12 @@ impl<'a> ParserImpl<'a> { mod test { use std::path::Path; - use oxc_ast::CommentKind; + use oxc_ast::{ast::Expression, CommentKind}; use super::*; #[test] - fn smoke_test() { + fn parse_program_smoke_test() { let allocator = Allocator::default(); let source_type = SourceType::default(); let source = ""; @@ -421,6 +452,15 @@ mod test { assert!(ret.errors.is_empty()); } + #[test] + fn parse_expression_smoke_test() { + let allocator = Allocator::default(); + let source_type = SourceType::default(); + let source = "a"; + let expr = Parser::new(&allocator, source, source_type).parse_expression().unwrap(); + assert!(matches!(expr, Expression::Identifier(_))); + } + #[test] fn flow_error() { let allocator = Allocator::default(); diff --git a/crates/oxc_parser/src/ts/list.rs b/crates/oxc_parser/src/ts/list.rs index 90cedd6d9d5bb..a0dfd8bb24daf 100644 --- a/crates/oxc_parser/src/ts/list.rs +++ b/crates/oxc_parser/src/ts/list.rs @@ -241,7 +241,7 @@ impl<'a> SeparatedList<'a> for TSImportAttributeList<'a> { }; p.expect(Kind::Colon)?; - let value = p.parse_expression()?; + let value = p.parse_expr()?; let element = TSImportAttribute { span: p.end_span(span), name, value }; self.elements.push(element); Ok(())