mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 20:28:58 +00:00
feat(minifier): add skeleton for ReplaceGlobalDefines ast pass (#3803)
This commit is contained in:
parent
58e54f4aea
commit
dd540c8f0f
16 changed files with 205 additions and 41 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1535,6 +1535,7 @@ dependencies = [
|
|||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_codegen",
|
||||
"oxc_diagnostics",
|
||||
"oxc_index",
|
||||
"oxc_parser",
|
||||
"oxc_semantic",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
crates/oxc_minifier/src/ast_passes/replace_global_defines.rs
Normal file
104
crates/oxc_minifier/src/ast_passes/replace_global_defines.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ mod code_removal;
|
|||
mod folding;
|
||||
mod precedence;
|
||||
mod remove_dead_code;
|
||||
mod replace_global_defines;
|
||||
|
|
|
|||
23
crates/oxc_minifier/tests/oxc/replace_global_defines.rs
Normal file
23
crates/oxc_minifier/tests/oxc/replace_global_defines.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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<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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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<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
|
||||
};
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Expression<'a>, Vec<OxcDiagnostic>> {
|
||||
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<Expression<'a>, Vec<OxcDiagnostic>> {
|
||||
// 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::<Vec<_>>();
|
||||
if !errors.is_empty() {
|
||||
return Err(errors);
|
||||
}
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn parse_program(&mut self) -> Result<Program<'a>> {
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
Loading…
Reference in a new issue