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

This commit is contained in:
Boshen 2024-06-21 13:53:59 +00:00
parent 58e54f4aea
commit dd540c8f0f
16 changed files with 205 additions and 41 deletions

1
Cargo.lock generated
View file

@ -1535,6 +1535,7 @@ dependencies = [
"oxc_allocator",
"oxc_ast",
"oxc_codegen",
"oxc_diagnostics",
"oxc_index",
"oxc_parser",
"oxc_semantic",

View file

@ -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 }

View file

@ -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};

View file

@ -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>,
}

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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,
};

View file

@ -2,3 +2,4 @@ mod code_removal;
mod folding;
mod precedence;
mod remove_dead_code;
mod replace_global_defines;

View 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);
}

View file

@ -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 {

View file

@ -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)

View file

@ -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))
}

View file

@ -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))
}

View file

@ -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));
}

View file

@ -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();

View file

@ -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(())