feat(minifier): local traverse ctx and generated minifier traverse (#19106)

## Summary
- generate a minifier-local traverse runtime in `oxc_minifier`
- introduce handwritten `MinifierTraverseCtx` and move prior `Ctx` functionality into it
- remove old `Ctx` usage and implement the needed traits on `MinifierTraverseCtx`
- wire minifier passes/compressor to use local traverse
- reuse `ancestor.rs` from `oxc_traverse` via path module in `oxc_minifier`

## Why
- avoids orphan-rule friction around implementing traits on the prior context type
- keeps minifier traversal concerns local to `oxc_minifier`

## Validation
- `just fmt`
- `cargo check -p oxc_ast_tools`
- `cargo check -p oxc_minifier`
- `cargo test -p oxc_minifier`
- `just lint`
This commit is contained in:
Boshen committed 2026-02-09 13:18:18 +00:00
1 parent a5b876631b
commit 500d071334
42 files changed
+13187 -302

No files matched your search

Generated
+2
View File
@@ -2089,6 +2089,7 @@ version = "0.112.0"
dependencies = [
"cow-utils",
"insta",
"itoa",
"javascript-globals",
"oxc_allocator",
"oxc_ast",
@@ -2104,6 +2105,7 @@ dependencies = [
"oxc_semantic",
"oxc_sourcemap",
"oxc_span",
"oxc_str",
"oxc_syntax",
"oxc_traverse",
"pico-args",
+2
View File
@@ -33,10 +33,12 @@ oxc_parser = { workspace = true }
oxc_regular_expression = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true }
oxc_str = { workspace = true }
oxc_syntax = { workspace = true }
oxc_traverse = { workspace = true }
cow-utils = { workspace = true }
itoa = { workspace = true }
rustc-hash = { workspace = true }
[dev-dependencies]
+2 -3
View File
@@ -1,10 +1,9 @@
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_semantic::{Scoping, SemanticBuilder};
use oxc_traverse::ReusableTraverseCtx;
use crate::{
CompressOptions,
CompressOptions, ReusableTraverseCtx,
peephole::{Normalize, NormalizeOptions, PeepholeOptimizations},
state::MinifierState,
};
@@ -70,7 +69,7 @@ impl<'a> Compressor<'a> {
fn run_in_loop(
max_iterations: Option<u8>,
program: &mut Program<'a>,
ctx: &mut ReusableTraverseCtx<'a, MinifierState<'a>>,
ctx: &mut ReusableTraverseCtx<'a>,
) -> u8 {
let mut iteration = 0u8;
loop {
+5
View File
@@ -0,0 +1,5 @@
#[expect(clippy::redundant_pub_crate)]
#[path = "../../../oxc_traverse/src/generated/ancestor.rs"]
pub mod ancestor;
pub mod traverse;
pub mod walk;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+7 -1
View File
@@ -45,12 +45,14 @@
//! See the [crate documentation](https://github.com/oxc-project/oxc/tree/main/crates/oxc_minifier) for more details.
mod compressor;
mod ctx;
pub(crate) mod generated;
mod keep_var;
mod minifier_traverse;
mod options;
mod peephole;
mod state;
mod symbol_value;
mod traverse_context;
use oxc_allocator::Allocator;
use oxc_ast::ast::Program;
@@ -63,6 +65,10 @@ use rustc_hash::FxHashMap;
pub use oxc_mangler::{MangleOptions, MangleOptionsKeepNames};
pub(crate) use crate::generated::traverse::Traverse;
#[doc(hidden)]
pub(crate) use crate::traverse_context::MinifierTraverseCtx as TraverseCtx;
pub(crate) use crate::traverse_context::ReusableMinifierTraverseCtx as ReusableTraverseCtx;
pub use crate::{compressor::Compressor, options::*};
#[derive(Debug, Clone)]
@@ -0,0 +1,26 @@
use std::ptr;
use oxc_ast::ast::Program;
use crate::{
ReusableTraverseCtx, Traverse,
generated::{ancestor::Ancestor, walk::walk_ast},
};
pub fn traverse_mut_with_ctx<'a, Tr: Traverse<'a>>(
traverser: &mut Tr,
program: &mut Program<'a>,
ctx: &mut ReusableTraverseCtx<'a>,
) {
let program = ptr::from_mut(program);
let ctx = ctx.get_mut();
debug_assert!(ctx.ancestors_depth() == 1);
debug_assert!(matches!(ctx.parent(), Ancestor::None));
// SAFETY: `program` originates from `&mut Program<'a>` and context stack invariants are checked.
unsafe { walk_ast(traverser, program, ctx) };
debug_assert!(ctx.ancestors_depth() == 1);
debug_assert!(matches!(ctx.parent(), Ancestor::None));
}
@@ -1,7 +1,7 @@
use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -13,10 +13,10 @@ impl<'a> PeepholeOptimizations {
///
/// `foo['bar']` -> `foo.bar`
/// `foo?.['bar']` -> `foo?.bar`
pub fn convert_to_dotted_properties(expr: &mut MemberExpression<'a>, ctx: &Ctx<'a, '_>) {
pub fn convert_to_dotted_properties(expr: &mut MemberExpression<'a>, ctx: &TraverseCtx<'a>) {
let MemberExpression::ComputedMemberExpression(e) = expr else { return };
let Expression::StringLiteral(s) = &e.expression else { return };
if Ctx::is_identifier_name_patched(&s.value) {
if TraverseCtx::is_identifier_name_patched(&s.value) {
let property = ctx.ast.identifier_name(s.span, s.value);
*expr =
MemberExpression::StaticMemberExpression(ctx.ast.alloc_static_member_expression(
@@ -31,7 +31,7 @@ impl<'a> PeepholeOptimizations {
if e.optional {
return;
}
if let Some(n) = Ctx::string_to_equivalent_number_value(v) {
if let Some(n) = TraverseCtx::string_to_equivalent_number_value(v) {
e.expression = ctx.ast.expression_numeric_literal(s.span, n, None, NumberBase::Decimal);
}
}
@@ -8,7 +8,7 @@ use oxc_ecmascript::{
use oxc_span::{GetSpan, SPAN};
use oxc_syntax::operator::{BinaryOperator, LogicalOperator};
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -17,7 +17,7 @@ use super::PeepholeOptimizations;
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeFoldConstants.java>
impl<'a> PeepholeOptimizations {
#[expect(clippy::float_cmp)]
pub fn fold_unary_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_unary_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::UnaryExpression(e) = expr else { return };
match e.operator {
// Do not fold `void 0` back to `undefined`.
@@ -37,7 +37,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn fold_static_member_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_static_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::StaticMemberExpression(e) = expr else { return };
// TODO: tryFoldObjectPropAccess(n, left, name)
if e.object.may_have_side_effects(ctx) {
@@ -49,7 +49,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn fold_computed_member_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_computed_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ComputedMemberExpression(e) = expr else { return };
// TODO: tryFoldObjectPropAccess(n, left, name)
if e.object.may_have_side_effects(ctx) || e.expression.may_have_side_effects(ctx) {
@@ -61,7 +61,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn fold_logical_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_logical_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::LogicalExpression(e) = expr else { return };
if let Some(changed) = match e.operator {
LogicalOperator::And | LogicalOperator::Or => Self::try_fold_and_or(e, ctx),
@@ -72,7 +72,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn fold_chain_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_chain_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ChainExpression(e) = expr else { return };
let left_expr = match &e.expression {
match_member_expression!(ChainElement) => {
@@ -104,7 +104,7 @@ impl<'a> PeepholeOptimizations {
/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/09094b551915a6487a980a783831cba58b5739d1/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L587)
pub fn try_fold_and_or(
logical_expr: &mut LogicalExpression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let op = logical_expr.operator;
debug_assert!(matches!(op, LogicalOperator::And | LogicalOperator::Or));
@@ -174,7 +174,7 @@ impl<'a> PeepholeOptimizations {
/// Try to fold a nullish coalesce `foo ?? bar`.
pub fn try_fold_coalesce(
logical_expr: &mut LogicalExpression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce);
let left = &logical_expr.left;
@@ -249,7 +249,7 @@ impl<'a> PeepholeOptimizations {
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn fold_binary_expr(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_binary_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
// TODO: tryReduceOperandsForOp
@@ -353,7 +353,7 @@ impl<'a> PeepholeOptimizations {
}
// Simplified version of `tryFoldAdd` from closure compiler.
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: &Ctx<'a, '_>) -> Option<Expression<'a>> {
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: &TraverseCtx<'a>) -> Option<Expression<'a>> {
if !e.may_have_side_effects(ctx)
&& let Some(v) = e.evaluate_value(ctx)
{
@@ -399,7 +399,7 @@ impl<'a> PeepholeOptimizations {
left_expr: &mut Expression<'a>,
right_expr: &mut Expression<'a>,
parent_span: Span,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if let Expression::TemplateLiteral(left) = left_expr {
// "`${a}b` + `x${y}`" => "`${a}bx${y}`"
@@ -486,7 +486,7 @@ impl<'a> PeepholeOptimizations {
fn try_fold_left_child_op(
e: &mut BinaryExpression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let op = e.operator;
debug_assert!(matches!(
@@ -521,7 +521,7 @@ impl<'a> PeepholeOptimizations {
))
}
pub fn fold_call_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_call_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(e) = expr else { return };
if !ctx.is_global_expr("Number", &e.callee) {
return;
@@ -561,7 +561,7 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
}
pub fn fold_binary_typeof_comparison(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn fold_binary_typeof_comparison(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
// `typeof a == typeof a` -> `true`, `typeof a != typeof a` -> `false`
if e.operator.is_equality()
@@ -620,8 +620,8 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn fold_object_exp(e: &mut ObjectExpression<'a>, ctx: &mut Ctx<'a, '_>) {
fn should_fold_spread_element<'a>(e: &Expression<'a>, ctx: &Ctx<'a, '_>) -> bool {
pub fn fold_object_exp(e: &mut ObjectExpression<'a>, ctx: &mut TraverseCtx<'a>) {
fn should_fold_spread_element<'a>(e: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
match e {
Expression::ArrayExpression(o) if o.elements.is_empty() => true,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => true,
@@ -692,7 +692,10 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
}
fn is_spread_inlineable_object_literal(e: &ObjectExpression<'a>, ctx: &Ctx<'a, '_>) -> bool {
fn is_spread_inlineable_object_literal(
e: &ObjectExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
e.properties.iter().all(|p| match p {
ObjectPropertyKind::SpreadProperty(_) => true,
ObjectPropertyKind::ObjectProperty(p) => {
@@ -712,7 +715,7 @@ impl<'a> PeepholeOptimizations {
/// Inline constant values in template literals
///
/// - `foo${1}bar${i}` => `foo1bar${i}`
pub fn inline_template_literal(t: &mut TemplateLiteral<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn inline_template_literal(t: &mut TemplateLiteral<'a>, ctx: &mut TraverseCtx<'a>) {
let has_expr_to_inline = t
.expressions
.iter()
+5 -5
View File
@@ -1,14 +1,14 @@
use crate::generated::ancestor::Ancestor;
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
use oxc_span::GetSpan;
use oxc_traverse::Ancestor;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut TraverseCtx<'a>) {
let BindingPattern::BindingIdentifier(ident) = &decl.id else { return };
let Some(symbol_id) = ident.symbol_id.get() else { return };
let value = if decl.kind.is_var() || Self::is_for_statement_init(ctx) {
@@ -21,11 +21,11 @@ impl<'a> PeepholeOptimizations {
ctx.init_value(symbol_id, value);
}
fn is_for_statement_init(ctx: &Ctx<'a, '_>) -> bool {
fn is_for_statement_init(ctx: &TraverseCtx<'a>) -> bool {
ctx.ancestors().nth(1).is_some_and(Ancestor::is_parent_of_for_statement_left)
}
pub fn inline_identifier_reference(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn inline_identifier_reference(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::Identifier(ident) = expr else { return };
let reference_id = ident.reference_id();
let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { return };
@@ -1,4 +1,4 @@
use crate::ctx::Ctx;
use crate::TraverseCtx;
use oxc_allocator::TakeIn;
use oxc_ast::{NONE, ast::*};
use oxc_compat::ESFeature;
@@ -16,7 +16,7 @@ impl<'a> PeepholeOptimizations {
test: Expression<'a>,
consequent: Expression<'a>,
alternate: Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let mut cond_expr = ctx.ast.conditional_expression(span, test, consequent, alternate);
Self::minimize_conditional_expression(&mut cond_expr, ctx)
@@ -26,7 +26,7 @@ impl<'a> PeepholeOptimizations {
/// `MangleIfExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2745>
pub fn minimize_conditional_expression(
expr: &mut ConditionalExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
match &mut expr.test {
// "(a, b) ? c : d" => "a, b ? c : d"
@@ -416,7 +416,7 @@ impl<'a> PeepholeOptimizations {
/// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1`
fn try_merge_conditional_expression_inside(
expr: &mut ConditionalExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let (
Expression::AssignmentExpression(consequent),
@@ -461,7 +461,7 @@ impl<'a> PeepholeOptimizations {
target_id_name: &str,
expr_to_inject: &mut Expression<'a>,
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> bool {
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
@@ -486,7 +486,7 @@ impl<'a> PeepholeOptimizations {
target_id_name: &str,
expr_to_inject: &mut Expression<'a>,
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> bool {
match expr {
Expression::StaticMemberExpression(e) => {
@@ -8,7 +8,7 @@ use oxc_ecmascript::{
use oxc_semantic::ReferenceFlags;
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -27,7 +27,7 @@ impl<'a> PeepholeOptimizations {
op: LogicalOperator,
a: Expression<'a>,
b: Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
// "(a, b) op c" => "a, b op c"
if let Expression::SequenceExpression(mut sequence_expr) = a {
@@ -67,7 +67,7 @@ impl<'a> PeepholeOptimizations {
// ^^^^^^^^^^^^^^ `ctx.expression_value_type(&e.left).is_boolean()` is `true`.
// `x >> +y !== 0` -> `x >> +y`
// ^^^^^^^ ctx.expression_value_type(&e.left).is_number()` is `true`.
pub fn minimize_binary(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_binary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
if !e.operator.is_equality() {
return;
@@ -124,7 +124,7 @@ impl<'a> PeepholeOptimizations {
///
/// In `IsLooselyEqual`, `true` and `false` are converted to `1` and `0` first.
/// <https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal>
pub fn minimize_loose_boolean(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_loose_boolean(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = e else { return };
if !matches!(e.operator, BinaryOperator::Equality | BinaryOperator::Inequality) {
return;
@@ -173,7 +173,7 @@ impl<'a> PeepholeOptimizations {
/// This can only be done for resolved identifiers as this would avoid setting `a` when `a` is truthy.
pub fn minimize_normal_assignment_to_combined_logical_assignment(
expr: &mut AssignmentExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators)
|| !matches!(expr.operator, AssignmentOperator::Assign)
@@ -212,7 +212,7 @@ impl<'a> PeepholeOptimizations {
/// Compress `a = a + b` to `a += b`
pub fn minimize_normal_assignment_to_combined_assignment(
expr: &mut AssignmentExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if !matches!(expr.operator, AssignmentOperator::Assign) {
return;
@@ -235,7 +235,7 @@ impl<'a> PeepholeOptimizations {
#[expect(clippy::float_cmp)]
pub fn minimize_assignment_to_update_expression(
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let Expression::AssignmentExpression(e) = expr else { return };
if !matches!(e.operator, AssignmentOperator::Subtraction) {
@@ -3,7 +3,7 @@ use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, IsInt32OrUint32};
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -13,7 +13,7 @@ impl<'a> PeepholeOptimizations {
/// `SimplifyBooleanExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2059>
pub fn minimize_expression_in_boolean_context(
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
match expr {
// "!!a" => "a"
@@ -2,13 +2,13 @@ use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
/// `mangleFor`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_parser.go#L9801>
pub fn minimize_for_statement(for_stmt: &mut ForStatement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_for_statement(for_stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
// Get the first statement in the loop
let mut first = &for_stmt.body;
if let Statement::BlockStatement(block_stmt) = first {
@@ -106,7 +106,7 @@ impl<'a> PeepholeOptimizations {
span: Span,
body: Option<Statement<'a>>,
replace: Option<Statement<'a>>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Statement<'a> {
match body {
Some(Statement::BlockStatement(mut block_stmt)) if !block_stmt.body.is_empty() => {
@@ -4,7 +4,7 @@ use oxc_ast::ast::*;
use oxc_semantic::ScopeFlags;
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -12,7 +12,7 @@ impl<'a> PeepholeOptimizations {
/// `MangleIf`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_parser/js_parser.go#L9860>
pub fn try_minimize_if(
if_stmt: &mut IfStatement<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
Self::wrap_to_avoid_ambiguous_else(if_stmt, ctx);
if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
@@ -126,7 +126,7 @@ impl<'a> PeepholeOptimizations {
/// Wrap to avoid ambiguous else.
/// `if (foo) if (bar) baz else quaz` -> `if (foo) { if (bar) baz else quaz }`
fn wrap_to_avoid_ambiguous_else(if_stmt: &mut IfStatement<'a>, ctx: &mut Ctx<'a, '_>) {
fn wrap_to_avoid_ambiguous_else(if_stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) {
if let Statement::IfStatement(if2) = &mut if_stmt.consequent
&& if2.consequent.is_jump_statement()
&& if2.alternate.is_some()
@@ -4,12 +4,12 @@ use oxc_compat::ESFeature;
use oxc_semantic::ReferenceFlags;
use oxc_span::{ContentEq, GetSpan, SPAN};
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn minimize_logical_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_logical_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::LogicalExpression(e) = expr else { return };
if let Some(changed) = Self::try_compress_is_null_or_undefined(e, ctx) {
*expr = changed;
@@ -32,7 +32,7 @@ impl<'a> PeepholeOptimizations {
/// - `document.all == null` is `true`
fn try_compress_is_null_or_undefined(
expr: &mut LogicalExpression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let op = expr.operator;
let target_ops = match op {
@@ -78,7 +78,7 @@ impl<'a> PeepholeOptimizations {
right: &mut Expression<'a>,
span: Span,
(find_op, replace_op): (BinaryOperator, BinaryOperator),
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
enum LeftPairValueResult {
Null(Span),
@@ -163,7 +163,7 @@ impl<'a> PeepholeOptimizations {
pub fn has_no_side_effect_for_evaluation_same_target(
assignment_target: &AssignmentTarget<'a>,
expr: &Expression,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
if let (
AssignmentTarget::AssignmentTargetIdentifier(write_id_ref),
@@ -207,7 +207,7 @@ impl<'a> PeepholeOptimizations {
/// Also `a || (foo, bar, a = b)` to `a ||= (foo, bar, b)`
fn try_compress_logical_expression_to_assignment_expression(
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators) {
return;
@@ -286,7 +286,10 @@ impl<'a> PeepholeOptimizations {
///
/// When creating AssignmentTargetIdentifier from normal expressions, the identifier only has ReferenceFlags::Write.
/// But assignment expressions changes the value, so we should add ReferenceFlags::Read.
pub fn mark_assignment_target_as_read(assign_target: &AssignmentTarget, ctx: &mut Ctx<'a, '_>) {
pub fn mark_assignment_target_as_read(
assign_target: &AssignmentTarget,
ctx: &mut TraverseCtx<'a>,
) {
if let AssignmentTarget::AssignmentTargetIdentifier(id) = assign_target {
let reference = ctx.scoping_mut().get_reference_mut(id.reference_id());
reference.flags_mut().insert(ReferenceFlags::Read);
@@ -3,19 +3,23 @@ use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::DetermineValueType;
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn minimize_not(span: Span, expr: Expression<'a>, ctx: &mut Ctx<'a, '_>) -> Expression<'a> {
pub fn minimize_not(
span: Span,
expr: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let mut unary = ctx.ast.expression_unary(span, UnaryOperator::LogicalNot, expr);
Self::minimize_unary(&mut unary, ctx);
unary
}
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_unary(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::UnaryExpression(e) = expr else { return };
if !e.operator.is_not() {
return;
@@ -1,5 +1,6 @@
use std::{iter, ops::ControlFlow};
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{Box, TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
@@ -9,9 +10,8 @@ use oxc_ecmascript::{
};
use oxc_semantic::ScopeFlags;
use oxc_span::{ContentEq, GetSpan};
use oxc_traverse::Ancestor;
use crate::{ctx::Ctx, keep_var::KeepVar};
use crate::{TraverseCtx, keep_var::KeepVar};
use super::PeepholeOptimizations;
@@ -33,7 +33,7 @@ impl<'a> PeepholeOptimizations {
///
/// ## MinimizeExitPoints:
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/MinimizeExitPoints.java>
pub fn minimize_statements(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut Ctx<'a, '_>) {
pub fn minimize_statements(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
let mut old_stmts = stmts.take_in(ctx.ast);
let mut is_control_flow_dead = false;
let mut keep_var = KeepVar::new(ctx.ast);
@@ -299,7 +299,7 @@ impl<'a> PeepholeOptimizations {
result: &mut Vec<'a, Statement<'a>>,
is_control_flow_dead: &mut bool,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> ControlFlow<()> {
match stmt {
Statement::EmptyStatement(_) => (),
@@ -349,7 +349,7 @@ impl<'a> PeepholeOptimizations {
fn join_sequence(
a: &mut Expression<'a>,
b: &mut Expression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let a = a.take_in(ctx.ast);
let b = b.take_in(ctx.ast);
@@ -384,7 +384,7 @@ impl<'a> PeepholeOptimizations {
mut var_decl: Box<'a, VariableDeclaration<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if let Some(first_decl) = var_decl.declarations.first_mut()
&& let Some(first_decl_init) = first_decl.init.as_mut()
@@ -447,7 +447,7 @@ impl<'a> PeepholeOptimizations {
mut expr_stmt: Box<'a, ExpressionStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let changed = Self::substitute_single_use_symbol_in_statement(
&mut expr_stmt.expression,
@@ -523,7 +523,7 @@ impl<'a> PeepholeOptimizations {
fn merge_assignment_to_declaration(
assign_expr: &mut AssignmentExpression<'a>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
if assign_expr.operator != AssignmentOperator::Assign {
return false;
@@ -576,7 +576,7 @@ impl<'a> PeepholeOptimizations {
mut switch_stmt: Box<'a, SwitchStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let changed = Self::substitute_single_use_symbol_in_statement(
&mut switch_stmt.discriminant,
@@ -637,7 +637,7 @@ impl<'a> PeepholeOptimizations {
mut if_stmt: Box<'a, IfStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> ControlFlow<()> {
let changed =
Self::substitute_single_use_symbol_in_statement(&mut if_stmt.test, result, ctx, false);
@@ -790,7 +790,7 @@ impl<'a> PeepholeOptimizations {
result: &mut Vec<'a, Statement<'a>>,
is_control_flow_dead: &mut bool,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if let Some(ret_argument_expr) = &mut ret_stmt.argument {
let changed = Self::substitute_single_use_symbol_in_statement(
@@ -846,7 +846,7 @@ impl<'a> PeepholeOptimizations {
result: &mut Vec<'a, Statement<'a>>,
is_control_flow_dead: &mut bool,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let changed = Self::substitute_single_use_symbol_in_statement(
&mut throw_stmt.argument,
@@ -875,7 +875,7 @@ impl<'a> PeepholeOptimizations {
mut for_stmt: Box<'a, ForStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if let Some(init) = &mut for_stmt.init {
match init {
@@ -976,7 +976,7 @@ impl<'a> PeepholeOptimizations {
mut for_in_stmt: Box<'a, ForInStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
// Annex B.3.5 allows initializers in non-strict mode
// <https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-initializers-in-forin-statement-heads>
@@ -1069,7 +1069,7 @@ impl<'a> PeepholeOptimizations {
fn handle_for_of_statement(
mut for_of_stmt: Box<'a, ForOfStatement<'a>>,
result: &mut Vec<'a, Statement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let is_block_scoped_decl = matches!(&for_of_stmt.left, ForStatementLeft::VariableDeclaration(var_decl) if !var_decl.kind.is_var());
let changed = Self::substitute_single_use_symbol_in_statement(
@@ -1114,7 +1114,7 @@ impl<'a> PeepholeOptimizations {
fn handle_block(
result: &mut Vec<'a, Statement<'a>>,
block_stmt: Box<'a, BlockStatement<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let keep_block = block_stmt.body.iter().any(Self::statement_cares_about_scope);
if keep_block {
@@ -1166,7 +1166,7 @@ impl<'a> PeepholeOptimizations {
fn substitute_single_use_symbol_in_statement(
expr_in_stmt: &mut Expression<'a>,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
non_scoped_literal_only: bool,
) -> bool {
if Self::keep_top_level_var_in_script_mode(ctx)
@@ -1204,7 +1204,7 @@ impl<'a> PeepholeOptimizations {
fn substitute_single_use_symbol_within_declaration(
kind: VariableDeclarationKind,
declarations: &mut Vec<'a, VariableDeclarator<'a>>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
if Self::keep_top_level_var_in_script_mode(ctx)
|| ctx.current_scope_flags().contains_direct_eval()
@@ -1240,7 +1240,7 @@ impl<'a> PeepholeOptimizations {
fn substitute_single_use_symbol_in_expression_from_declarators(
target_expr: &mut Expression<'a>,
declarators: &mut [VariableDeclarator<'a>],
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
non_scoped_literal_only: bool,
) -> usize {
let last_non_inlined_index = declarators.iter_mut().rposition(|prev_decl| {
@@ -1297,7 +1297,7 @@ impl<'a> PeepholeOptimizations {
search_for: &str,
replacement: &mut Expression<'a>,
replacement_has_side_effect: bool,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<bool> {
match target_expr {
Expression::Identifier(id) => {
+7 -36
View File
@@ -23,12 +23,8 @@ use rustc_hash::FxHashSet;
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_traverse::{ReusableTraverseCtx, Traverse, traverse_mut_with_ctx};
use crate::{
ctx::{Ctx, TraverseCtx},
state::MinifierState,
};
use crate::{ReusableTraverseCtx, Traverse, TraverseCtx, minifier_traverse::traverse_mut_with_ctx};
pub use self::normalize::{Normalize, NormalizeOptions};
@@ -36,11 +32,7 @@ pub use self::normalize::{Normalize, NormalizeOptions};
pub struct PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn run_once(
&mut self,
program: &mut Program<'a>,
ctx: &mut ReusableTraverseCtx<'a, MinifierState<'a>>,
) {
pub fn run_once(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
traverse_mut_with_ctx(self, program, ctx);
}
@@ -79,7 +71,7 @@ impl<'a> PeepholeOptimizations {
/// which would change the semantics.
pub fn member_object_may_be_mutated(
assignment_target: &AssignmentTarget<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
let object = match assignment_target {
AssignmentTarget::ComputedMemberExpression(member_expr) => &member_expr.object,
@@ -97,7 +89,7 @@ impl<'a> PeepholeOptimizations {
/// or if the expression is not a simple identifier/this reference.
pub fn is_expression_that_reference_may_change(
expr: &Expression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
match expr {
Expression::Identifier(id) => {
@@ -114,7 +106,7 @@ impl<'a> PeepholeOptimizations {
}
}
impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
impl<'a> Traverse<'a> for PeepholeOptimizations {
fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
ctx.state.symbol_values.clear();
ctx.state.changed = false;
@@ -136,7 +128,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
}
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
let ctx = &mut Ctx::new(ctx);
Self::minimize_statements(stmts, ctx);
}
@@ -144,12 +135,10 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::keep_track_of_pure_functions(stmt, ctx);
}
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = &mut Ctx::new(ctx);
if ctx.state.dce {
match stmt {
Statement::BlockStatement(_) => Self::try_optimize_block(stmt, ctx),
@@ -213,7 +202,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_for_statement(stmt, ctx);
Self::minimize_for_statement(stmt, ctx);
}
@@ -222,7 +210,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_return_statement(stmt, ctx);
}
@@ -234,7 +221,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_variable_declaration(decl, ctx);
}
@@ -243,12 +229,10 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
decl: &mut VariableDeclarator<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let ctx = &mut Ctx::new(ctx);
Self::init_symbol_value(decl, ctx);
}
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = &mut Ctx::new(ctx);
if ctx.state.dce {
match expr {
Expression::TemplateLiteral(t) => {
@@ -370,7 +354,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
return;
}
if expr.operator.is_not() {
let ctx = &mut Ctx::new(ctx);
Self::minimize_expression_in_boolean_context(&mut expr.argument, ctx);
}
}
@@ -379,7 +362,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_call_expression(e, ctx);
Self::remove_empty_spread_arguments(&mut e.arguments);
}
@@ -388,7 +370,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_new_expression(e, ctx);
Self::remove_empty_spread_arguments(&mut e.arguments);
}
@@ -397,7 +378,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_object_property(prop, ctx);
}
@@ -409,7 +389,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_assignment_target_property(node, ctx);
}
@@ -421,7 +400,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_assignment_target_property_property(prop, ctx);
}
@@ -429,7 +407,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_binding_property(prop, ctx);
}
@@ -441,7 +418,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_method_definition(prop, ctx);
}
@@ -453,7 +429,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_property_definition(prop, ctx);
}
@@ -465,7 +440,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::substitute_accessor_property(prop, ctx);
}
@@ -477,8 +451,7 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = Ctx::new(ctx);
Self::convert_to_dotted_properties(expr, &ctx);
Self::convert_to_dotted_properties(expr, ctx);
}
fn enter_class_body(&mut self, _body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
@@ -492,7 +465,6 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = &mut Ctx::new(ctx);
Self::remove_dead_code_exit_class_body(body, ctx);
Self::remove_unused_private_members(body, ctx);
ctx.state.class_symbols_stack.pop_class_scope(Self::get_declared_private_symbols(body));
@@ -502,8 +474,7 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
if ctx.state.dce {
return;
}
let ctx = Ctx::new(ctx);
Self::substitute_catch_clause(catch, &ctx);
Self::substitute_catch_clause(catch, ctx);
}
fn exit_private_field_expression(
+10 -18
View File
@@ -1,3 +1,4 @@
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_ecmascript::{
@@ -7,12 +8,8 @@ use oxc_ecmascript::{
use oxc_semantic::IsGlobalReference;
use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::{Ancestor, ReusableTraverseCtx, Traverse, traverse_mut_with_ctx};
use crate::{
ctx::{Ctx, TraverseCtx},
state::MinifierState,
};
use crate::{ReusableTraverseCtx, Traverse, TraverseCtx, minifier_traverse::traverse_mut_with_ctx};
#[derive(Default)]
pub struct NormalizeOptions {
@@ -46,16 +43,12 @@ pub struct Normalize {
}
impl<'a> Normalize {
pub fn build(
&mut self,
program: &mut Program<'a>,
ctx: &mut ReusableTraverseCtx<'a, MinifierState<'a>>,
) {
pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
traverse_mut_with_ctx(self, program, ctx);
}
}
impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
impl<'a> Traverse<'a> for Normalize {
fn exit_program(&mut self, node: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) {
if self.options.remove_unnecessary_use_strict && node.source_type.is_module() {
node.directives.drain_filter(|d| d.directive.as_str() == "use strict");
@@ -271,7 +264,7 @@ impl<'a> Normalize {
fn fold_number_nan_to_nan(
e: &StaticMemberExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let Expression::Identifier(ident) = &e.object else { return None };
if ident.name != "Number" {
@@ -280,7 +273,7 @@ impl<'a> Normalize {
if e.property.name != "NaN" {
return None;
}
if !Ctx::new(ctx).is_global_reference(ident) {
if !ctx.is_global_reference(ident) {
return None;
}
Some(ctx.ast.nan(ident.span))
@@ -305,7 +298,7 @@ impl<'a> Normalize {
/// `PC` or `PC_WITH_ARRAY` in <https://github.com/rollup/rollup/blob/v4.42.0/src/ast/nodes/shared/knownGlobals.ts>
fn set_pure_or_no_side_effects_to_new_expr(
new_expr: &mut NewExpression<'a>,
ctx: &mut TraverseCtx<'a>,
ctx: &TraverseCtx<'a>,
) {
if new_expr.pure {
return;
@@ -321,7 +314,6 @@ impl<'a> Normalize {
return;
}
// callee is a global reference.
let ctx = Ctx::new(ctx);
let len = new_expr.arguments.len();
let (zero_arg_throws_error, one_arg_array_throws_error, one_arg_throws_error): (
@@ -371,7 +363,7 @@ impl<'a> Normalize {
| "TypeError" | "URIError" | "Number" | "Object" | "String" => (false, false, &[]),
// RegExp needs special validation using the regex parser
"RegExp" => {
if Self::can_set_pure(ident, &ctx) && is_valid_regexp(&new_expr.arguments) {
if Self::can_set_pure(ident, ctx) && is_valid_regexp(&new_expr.arguments) {
new_expr.pure = true;
}
return;
@@ -379,7 +371,7 @@ impl<'a> Normalize {
_ => return,
};
if !Self::can_set_pure(ident, &ctx) {
if !Self::can_set_pure(ident, ctx) {
return;
}
@@ -409,7 +401,7 @@ impl<'a> Normalize {
}
}
fn can_set_pure(ident: &IdentifierReference<'a>, ctx: &Ctx<'a, '_>) -> bool {
fn can_set_pure(ident: &IdentifierReference<'a>, ctx: &TraverseCtx<'a>) -> bool {
ctx.is_global_reference(ident)
// Throw is never pure.
&& !matches!(ctx.parent(), Ancestor::ThrowStatementArgument(_))
@@ -1,3 +1,4 @@
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
@@ -6,9 +7,8 @@ use oxc_ecmascript::{
side_effects::MayHaveSideEffects,
};
use oxc_span::GetSpan;
use oxc_traverse::Ancestor;
use crate::{ctx::Ctx, keep_var::KeepVar};
use crate::{TraverseCtx, keep_var::KeepVar};
use super::PeepholeOptimizations;
@@ -21,7 +21,7 @@ use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
/// Remove block from single line blocks
/// `{ block } -> block`
pub fn try_optimize_block(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_optimize_block(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::BlockStatement(s) = stmt else { return };
match s.body.len() {
0 => {
@@ -54,7 +54,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn try_fold_if(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_if(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::IfStatement(if_stmt) = stmt else { return };
// Descend and remove `else` blocks first.
match &mut if_stmt.alternate {
@@ -123,7 +123,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn try_fold_for(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_for(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::ForStatement(for_stmt) = stmt else { return };
if let Some(init) = &mut for_stmt.init
&& let Some(init) = init.as_expression_mut()
@@ -194,7 +194,7 @@ impl<'a> PeepholeOptimizations {
/// ```js
/// a: break a;
/// ```
pub fn try_fold_labeled(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_labeled(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::LabeledStatement(s) = stmt else { return };
let id = s.label.name.as_str();
@@ -224,7 +224,7 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
}
pub fn try_fold_expression_stmt(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_expression_stmt(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::ExpressionStatement(expr_stmt) = stmt else { return };
// We need to check if it is in arrow function with `expression: true`.
// This is the only scenario where we can't remove it even if `ExpressionStatement`.
@@ -240,7 +240,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn try_fold_try(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_try(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::TryStatement(s) = stmt else { return };
if let Some(handler) = &s.handler
&& s.block.body.is_empty()
@@ -276,7 +276,7 @@ impl<'a> PeepholeOptimizations {
}
/// Try folding conditional expression (?:) if the condition results of the condition is known.
pub fn try_fold_conditional_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_fold_conditional_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ConditionalExpression(e) = expr else { return };
let Some(v) = e.test.evaluate_value_to_boolean(ctx) else { return };
ctx.state.changed = true;
@@ -310,7 +310,7 @@ impl<'a> PeepholeOptimizations {
};
}
pub fn remove_sequence_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_sequence_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::SequenceExpression(e) = expr else { return };
let should_keep_as_sequence_expr = e
.expressions
@@ -352,7 +352,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn keep_track_of_pure_functions(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn keep_track_of_pure_functions(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
match stmt {
Statement::FunctionDeclaration(f) => {
if let Some(body) = &f.body {
@@ -407,7 +407,7 @@ impl<'a> PeepholeOptimizations {
body: &FunctionBody<'a>,
r#async: bool,
generator: bool,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if r#async || generator {
return;
@@ -428,7 +428,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn remove_dead_code_call_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_dead_code_call_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(e) = expr else { return };
if let Expression::Identifier(ident) = &e.callee {
let reference_id = ident.reference_id();
@@ -456,7 +456,10 @@ impl<'a> PeepholeOptimizations {
/// Example case: `let o = { f() { assert.ok(this !== o); } }; (true && o.f)(); (true && o.f)``;`
///
/// * `access_value` - The expression that may need to be kept as indirect reference (`foo.bar` in the example above)
pub fn should_keep_indirect_access(access_value: &Expression<'a>, ctx: &Ctx<'a, '_>) -> bool {
pub fn should_keep_indirect_access(
access_value: &Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
match ctx.parent() {
Ancestor::CallExpressionCallee(_) | Ancestor::TaggedTemplateExpressionTag(_) => {
match access_value {
@@ -496,7 +499,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn remove_dead_code_exit_class_body(body: &mut ClassBody<'a>, _ctx: &mut Ctx<'a, '_>) {
pub fn remove_dead_code_exit_class_body(body: &mut ClassBody<'a>, _ctx: &mut TraverseCtx<'a>) {
body.body.retain(|e| !matches!(e, ClassElement::StaticBlock(s) if s.body.is_empty()));
}
@@ -1,16 +1,16 @@
use super::PeepholeOptimizations;
use crate::{CompressOptionsUnused, ctx::Ctx};
use crate::{CompressOptionsUnused, TraverseCtx};
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{DetermineValueType, ValueType};
impl<'a> PeepholeOptimizations {
fn can_remove_unused_declarators(ctx: &Ctx<'a, '_>) -> bool {
fn can_remove_unused_declarators(ctx: &TraverseCtx<'a>) -> bool {
ctx.state.options.unused != CompressOptionsUnused::Keep
&& !Self::keep_top_level_var_in_script_mode(ctx)
&& !ctx.scoping().root_scope_flags().contains_direct_eval()
}
fn is_sync_iterator_expr(expr: &Expression<'a>, ctx: &Ctx<'a, '_>) -> bool {
fn is_sync_iterator_expr(expr: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
match expr {
Expression::ArrayExpression(_)
| Expression::StringLiteral(_)
@@ -32,7 +32,7 @@ impl<'a> PeepholeOptimizations {
pub fn should_remove_unused_declarator(
decl: &VariableDeclarator<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> bool {
if !Self::can_remove_unused_declarators(ctx) {
return false;
@@ -67,7 +67,7 @@ impl<'a> PeepholeOptimizations {
pub fn remove_unused_variable_declaration(
mut stmt: Statement<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Statement<'a>> {
let Statement::VariableDeclaration(var_decl) = &mut stmt else { return Some(stmt) };
if !Self::can_remove_unused_declarators(ctx) {
@@ -80,7 +80,7 @@ impl<'a> PeepholeOptimizations {
Some(stmt)
}
pub fn remove_unused_function_declaration(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_unused_function_declaration(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::FunctionDeclaration(f) = stmt else { return };
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return;
@@ -99,7 +99,7 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
}
pub fn remove_unused_class_declaration(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_unused_class_declaration(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
let Statement::ClassDeclaration(c) = stmt else { return };
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return;
@@ -128,7 +128,7 @@ impl<'a> PeepholeOptimizations {
}
/// Do remove top level vars in script mode.
pub fn keep_top_level_var_in_script_mode(ctx: &Ctx<'a, '_>) -> bool {
pub fn keep_top_level_var_in_script_mode(ctx: &TraverseCtx<'a>) -> bool {
ctx.scoping.current_scope_id() == ctx.scoping().root_scope_id()
&& ctx.source_type().is_script()
}
@@ -156,7 +156,7 @@ impl<'a> PeepholeOptimizations {
/// import 'a'
/// import 'b'
/// ```
pub fn remove_unused_import_specifiers(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_unused_import_specifiers(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
if ctx.options().treeshake.invalid_import_side_effects
|| ctx.state.options.unused == CompressOptionsUnused::Keep
{
@@ -1,6 +1,6 @@
use std::iter;
use crate::{CompressOptionsUnused, ctx::Ctx};
use crate::{CompressOptionsUnused, TraverseCtx};
use oxc_allocator::{TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_compat::ESFeature;
@@ -14,7 +14,7 @@ use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
/// `SimplifyUnusedExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L534>
pub fn remove_unused_expression(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
pub fn remove_unused_expression(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
match e {
Expression::ArrayExpression(_) => Self::remove_unused_array_expr(e, ctx),
Expression::AssignmentExpression(_) => Self::remove_unused_assignment_expr(e, ctx),
@@ -32,7 +32,7 @@ impl<'a> PeepholeOptimizations {
}
}
fn remove_unused_unary_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_unary_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::UnaryExpression(unary_expr) = e else { return false };
match unary_expr.operator {
UnaryOperator::Void | UnaryOperator::LogicalNot => {
@@ -53,7 +53,7 @@ impl<'a> PeepholeOptimizations {
}
}
fn remove_unused_sequence_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_sequence_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::SequenceExpression(sequence_expr) = e else { return false };
let old_len = sequence_expr.expressions.len();
sequence_expr.expressions.retain_mut(|e| !Self::remove_unused_expression(e, ctx));
@@ -63,7 +63,7 @@ impl<'a> PeepholeOptimizations {
sequence_expr.expressions.is_empty()
}
fn remove_unused_logical_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_logical_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
if !e.may_have_side_effects(ctx) {
return true;
}
@@ -175,7 +175,7 @@ impl<'a> PeepholeOptimizations {
}
// `([1,2,3, foo()])` -> `foo()`
fn remove_unused_array_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_array_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::ArrayExpression(array_expr) = e else {
return false;
};
@@ -225,7 +225,7 @@ impl<'a> PeepholeOptimizations {
false
}
fn remove_unused_new_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_new_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::NewExpression(new_expr) = e else { return false };
if (new_expr.pure && ctx.annotations()) || ctx.manual_pure_functions(&new_expr.callee) {
let mut exprs =
@@ -245,7 +245,7 @@ impl<'a> PeepholeOptimizations {
}
// "`${1}2${foo()}3`" -> "`${foo()}`"
fn remove_unused_template_literal(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_template_literal(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::TemplateLiteral(temp_lit) = e else { return false };
if temp_lit.expressions.is_empty() {
return true;
@@ -327,7 +327,7 @@ impl<'a> PeepholeOptimizations {
}
// `({ 1: 1, [foo()]: bar() })` -> `foo(), bar()`
fn remove_unused_object_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_object_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::ObjectExpression(object_expr) = e else {
return false;
};
@@ -392,7 +392,7 @@ impl<'a> PeepholeOptimizations {
false
}
fn remove_unused_conditional_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_conditional_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
if !e.may_have_side_effects(ctx) {
return true;
}
@@ -443,7 +443,7 @@ impl<'a> PeepholeOptimizations {
false
}
fn remove_unused_binary_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_binary_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::BinaryExpression(binary_expr) = e else {
return false;
};
@@ -493,7 +493,7 @@ impl<'a> PeepholeOptimizations {
}
/// returns whether the passed expression is a string
fn fold_string_addition_chain(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn fold_string_addition_chain(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::BinaryExpression(binary_expr) = e else {
return e.to_primitive(ctx).is_string() == Some(true);
};
@@ -536,7 +536,7 @@ impl<'a> PeepholeOptimizations {
false
}
fn remove_unused_call_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_call_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::CallExpression(call_expr) = e else { return false };
let is_pure = {
@@ -572,7 +572,7 @@ impl<'a> PeepholeOptimizations {
pub fn fold_arguments_into_needed_expressions(
args: &mut Vec<'a, Argument<'a>>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Vec<'a, Expression<'a>> {
ctx.ast.vec_from_iter(args.drain(..).filter_map(|arg| {
let mut expr = match arg {
@@ -586,7 +586,10 @@ impl<'a> PeepholeOptimizations {
}))
}
pub fn remove_unused_assignment_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
pub fn remove_unused_assignment_expr(
e: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> bool {
let Expression::AssignmentExpression(assign_expr) = e else { return false };
if matches!(
ctx.state.options.unused,
@@ -627,7 +630,7 @@ impl<'a> PeepholeOptimizations {
false
}
fn remove_unused_class_expr(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
fn remove_unused_class_expr(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
let Expression::ClassExpression(c) = e else { return false };
if let Some(exprs) = Self::remove_unused_class(c, ctx) {
if exprs.is_empty() {
@@ -640,7 +643,7 @@ impl<'a> PeepholeOptimizations {
pub fn remove_unused_class(
c: &mut Class<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Vec<'a, Expression<'a>>> {
// TypeError `class C extends (() => {}) {}`
if c.super_class
@@ -1,7 +1,7 @@
use oxc_ast::ast::*;
use oxc_ecmascript::side_effects::MayHaveSideEffects;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -10,7 +10,7 @@ impl<'a> PeepholeOptimizations {
///
/// This function uses the private member usage collected during the main traverse
/// to remove unused private fields and methods from the class body.
pub fn remove_unused_private_members(body: &mut ClassBody<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn remove_unused_private_members(body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
if ctx.current_scope_flags().contains_direct_eval() {
return;
}
@@ -2,6 +2,7 @@ use std::borrow::Cow;
use cow_utils::CowUtils;
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{Box, TakeIn};
use oxc_ast::{NONE, ast::*};
use oxc_compat::ESFeature;
@@ -14,9 +15,8 @@ use oxc_regular_expression::{
RegexUnsupportedPatterns, has_unsupported_regular_expression_pattern,
};
use oxc_span::SPAN;
use oxc_traverse::Ancestor;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -25,7 +25,7 @@ type Arguments<'a> = oxc_allocator::Vec<'a, Argument<'a>>;
/// Minimize With Known Methods
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeReplaceKnownMethods.java>
impl<'a> PeepholeOptimizations {
pub fn replace_known_global_methods(node: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn replace_known_global_methods(node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(ce) = node else { return };
// Use constant evaluation for known method calls
@@ -66,7 +66,7 @@ impl<'a> PeepholeOptimizations {
span: Span,
arguments: &mut Arguments<'a>,
object: &Expression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if !ctx.supports_feature(ESFeature::ES2016ExponentiationOperator) {
return None;
@@ -104,7 +104,7 @@ impl<'a> PeepholeOptimizations {
arguments: &mut Arguments<'a>,
name: &str,
object: &Expression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if !Self::validate_global_reference(object, "Array", ctx) {
return None;
@@ -120,7 +120,7 @@ impl<'a> PeepholeOptimizations {
/// `[].concat(a).concat(b)` -> `[].concat(a, b)`
/// `"".concat(a).concat(b)` -> `"".concat(a, b)`
pub fn replace_concat_chain(node: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn replace_concat_chain(node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let original_span = if let Expression::CallExpression(root_call_expr) = node {
root_call_expr.span
} else {
@@ -206,7 +206,7 @@ impl<'a> PeepholeOptimizations {
span: Span,
args: &mut Arguments<'a>,
callee: &mut Expression<'a>,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
// let concat chaining reduction handle it first
if let Ancestor::StaticMemberExpressionObject(parent_member) = ctx.parent()
@@ -361,7 +361,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn replace_known_property_access(node: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn replace_known_property_access(node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
// property access should be kept to keep `this` value
if matches!(
ctx.parent(),
@@ -484,7 +484,7 @@ impl<'a> PeepholeOptimizations {
fn try_fold_number_constants(
name: &str,
span: Span,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let num = |span: Span, n: f64| {
ctx.ast.expression_numeric_literal(span, n, None, NumberBase::Decimal)
@@ -550,7 +550,7 @@ impl<'a> PeepholeOptimizations {
object: &mut Expression<'a>,
property: u32,
span: Span,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if object.may_have_side_effects(ctx) {
return None;
@@ -589,7 +589,11 @@ impl<'a> PeepholeOptimizations {
}
}
fn validate_global_reference(expr: &Expression<'a>, target: &str, ctx: &Ctx<'a, '_>) -> bool {
fn validate_global_reference(
expr: &Expression<'a>,
target: &str,
ctx: &TraverseCtx<'a>,
) -> bool {
let Expression::Identifier(ident) = expr else { return false };
ctx.is_global_reference(ident) && ident.name == target
}
@@ -1,5 +1,6 @@
use std::iter::repeat_with;
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{CloneIn, TakeIn, Vec};
use oxc_ast::{NONE, ast::*};
use oxc_compat::ESFeature;
@@ -14,9 +15,8 @@ use oxc_syntax::{
number::NumberBase,
operator::{BinaryOperator, UnaryOperator},
};
use oxc_traverse::Ancestor;
use crate::ctx::Ctx;
use crate::TraverseCtx;
use super::PeepholeOptimizations;
@@ -25,7 +25,7 @@ use super::PeepholeOptimizations;
/// with literals, and simplifying returns.
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntax.java>
impl<'a> PeepholeOptimizations {
pub fn substitute_object_property(prop: &mut ObjectProperty<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_object_property(prop: &mut ObjectProperty<'a>, ctx: &mut TraverseCtx<'a>) {
// <https://tc39.es/ecma262/2024/multipage/ecmascript-language-expressions.html#sec-runtime-semantics-propertydefinitionevaluation>
if !prop.method
&& let PropertyKey::StringLiteral(str) = &prop.key
@@ -41,21 +41,21 @@ impl<'a> PeepholeOptimizations {
pub fn substitute_assignment_target_property_property(
prop: &mut AssignmentTargetPropertyProperty<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
Self::try_compress_property_key(&mut prop.name, &mut prop.computed, ctx);
}
pub fn substitute_assignment_target_property(
prop: &mut AssignmentTargetProperty<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
Self::try_compress_assignment_target_property(prop, ctx);
}
pub fn try_compress_assignment_target_property(
prop: &mut AssignmentTargetProperty<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
// `a: a` -> `a`
if let AssignmentTargetProperty::AssignmentTargetPropertyProperty(assign_target_prop_prop) =
@@ -76,11 +76,14 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn substitute_binding_property(prop: &mut BindingProperty<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_binding_property(prop: &mut BindingProperty<'a>, ctx: &mut TraverseCtx<'a>) {
Self::try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
}
pub fn substitute_method_definition(prop: &mut MethodDefinition<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_method_definition(
prop: &mut MethodDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let property_key_parent: ClassPropertyKeyParent = prop.into();
// Only check for computed property restrictions if this is actually a computed property
if prop.computed
@@ -94,7 +97,7 @@ impl<'a> PeepholeOptimizations {
pub fn substitute_property_definition(
prop: &mut PropertyDefinition<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let property_key_parent: ClassPropertyKeyParent = prop.into();
// Only check for computed property restrictions if this is actually a computed property
@@ -107,7 +110,10 @@ impl<'a> PeepholeOptimizations {
Self::try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
}
pub fn substitute_accessor_property(prop: &mut AccessorProperty<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_accessor_property(
prop: &mut AccessorProperty<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let property_key_parent: ClassPropertyKeyParent = prop.into();
// Only check for computed property restrictions if this is actually a computed property
if prop.computed
@@ -119,29 +125,29 @@ impl<'a> PeepholeOptimizations {
Self::try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
}
pub fn substitute_for_statement(stmt: &mut ForStatement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_for_statement(stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
Self::try_rewrite_arguments_copy_loop(stmt, ctx);
}
pub fn substitute_variable_declaration(
decl: &mut VariableDeclaration<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
for declarator in &mut decl.declarations {
Self::compress_variable_declarator(declarator, ctx);
}
}
pub fn substitute_call_expression(expr: &mut CallExpression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_call_expression(expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) {
Self::try_flatten_arguments(&mut expr.arguments, ctx);
Self::try_rewrite_object_callee_indirect_call(expr, ctx);
}
pub fn substitute_new_expression(expr: &mut NewExpression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_new_expression(expr: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) {
Self::try_flatten_arguments(&mut expr.arguments, ctx);
}
pub fn substitute_chain_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_chain_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ChainExpression(e) = expr else { return };
Self::try_flatten_nested_chain_expression(e, ctx);
Self::substitute_chain_call_expression(e, ctx);
@@ -159,7 +165,7 @@ impl<'a> PeepholeOptimizations {
/// `() => { return foo })` -> `() => foo`
pub fn substitute_arrow_expression(
arrow_expr: &mut ArrowFunctionExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
if !arrow_expr.expression
&& arrow_expr.body.directives.is_empty()
@@ -186,7 +192,7 @@ impl<'a> PeepholeOptimizations {
/// - `typeof foo.bar != "undefined"` -> `foo.bar !== undefined` (for any expression e.g.`typeof (foo + "")`)
///
/// Enabled by `compress.typeofs`
pub fn substitute_typeof_undefined(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_typeof_undefined(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
let Expression::UnaryExpression(unary_expr) = &e.left else { return };
if !unary_expr.operator.is_typeof() {
@@ -227,7 +233,7 @@ impl<'a> PeepholeOptimizations {
///
/// - `1 - +b` => `1 - b` (for other operators as well)
/// - `+a - 1` => `a - 1` (for other operators as well)
pub fn substitute_unary_plus(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_unary_plus(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::UnaryExpression(e) = expr else { return };
if e.operator != UnaryOperator::UnaryPlus {
return;
@@ -293,7 +299,10 @@ impl<'a> PeepholeOptimizations {
}
/// `a || (b || c);` -> `(a || b) || c;`
pub fn substitute_rotate_logical_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_rotate_logical_expression(
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let Expression::LogicalExpression(e) = expr else { return };
let Expression::LogicalExpression(right) = &e.right else { return };
if right.operator != e.operator {
@@ -318,7 +327,10 @@ impl<'a> PeepholeOptimizations {
/// Rotate commutative operators to reduce parentheses:
/// - `a * (b % c)` -> `b % c * a`
/// - `a * (b / c)` -> `b / c * a`
pub fn substitute_rotate_binary_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_rotate_binary_expression(
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let Expression::BinaryExpression(e) = expr else { return };
// Handle associative rotation
@@ -390,7 +402,7 @@ impl<'a> PeepholeOptimizations {
/// - If `foo` is an object, then `!!foo` is `true`. If `foo` is null, then `!!foo` is `false`.
///
/// This compression is safe for `document.all` because `typeof document.all` is not `'object'`.
pub fn substitute_is_object_and_not_null(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_is_object_and_not_null(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::LogicalExpression(e) = expr else { return };
let inversed = match e.operator {
LogicalOperator::And => false,
@@ -431,7 +443,7 @@ impl<'a> PeepholeOptimizations {
left: &Expression<'a>,
right: &Expression<'a>,
span: Span,
ctx: &Ctx<'a, '_>,
ctx: &TraverseCtx<'a>,
inversed: bool,
) -> Option<Expression<'a>> {
let pair = Self::commutative_pair(
@@ -530,7 +542,7 @@ impl<'a> PeepholeOptimizations {
))
}
pub fn substitute_loose_equals_undefined(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_loose_equals_undefined(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
// `foo == void 0` -> `foo == null`, `foo == undefined` -> `foo == null`
// `foo != void 0` -> `foo == null`, `foo == undefined` -> `foo == null`
@@ -573,7 +585,7 @@ impl<'a> PeepholeOptimizations {
/// for (var r = [], a = 1; a < arguments.length; a++)
/// r[a - 1] = arguments[a];
/// ```
fn try_rewrite_arguments_copy_loop(for_stmt: &mut ForStatement<'a>, ctx: &mut Ctx<'a, '_>) {
fn try_rewrite_arguments_copy_loop(for_stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
#[derive(PartialEq, Eq)]
enum VerifyArrayArgResult {
WithOffset,
@@ -924,7 +936,7 @@ impl<'a> PeepholeOptimizations {
///
/// `return undefined` -> `return`
/// `return void 0` -> `return`
pub fn substitute_return_statement(stmt: &mut ReturnStatement<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_return_statement(stmt: &mut ReturnStatement<'a>, ctx: &mut TraverseCtx<'a>) {
let Some(argument) = &stmt.argument else { return };
if !match argument {
Expression::Identifier(ident) => ctx.is_identifier_undefined(ident),
@@ -943,7 +955,7 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
}
fn compress_variable_declarator(decl: &mut VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) {
fn compress_variable_declarator(decl: &mut VariableDeclarator<'a>, ctx: &mut TraverseCtx<'a>) {
// Destructuring Pattern has error throwing side effect.
if matches!(
decl.kind,
@@ -967,7 +979,7 @@ impl<'a> PeepholeOptimizations {
/// `Number(0)` -> `0`
/// `String()` -> `''`
/// `BigInt(1)` -> `1`
pub fn substitute_simple_function_call(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_simple_function_call(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(e) = expr else { return };
if e.optional || e.arguments.len() >= 2 {
return;
@@ -1041,7 +1053,10 @@ impl<'a> PeepholeOptimizations {
}
/// Fold `Object` or `Array` constructor
fn get_fold_constructor_name(callee: &Expression<'a>, ctx: &Ctx<'a, '_>) -> Option<&'a str> {
fn get_fold_constructor_name(
callee: &Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<&'a str> {
match callee {
Expression::StaticMemberExpression(e) => {
if !matches!(&e.object, Expression::Identifier(ident) if ident.name == "window") {
@@ -1067,7 +1082,7 @@ impl<'a> PeepholeOptimizations {
/// `window.Array()`, `new Array()`, `Array()` -> `[]`
pub fn substitute_object_or_array_constructor(
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let callee = match expr {
Expression::NewExpression(e) => &e.callee,
@@ -1163,7 +1178,7 @@ impl<'a> PeepholeOptimizations {
/// `new AggregateError()` -> `AggregateError()`
/// `new Function()` -> `Function()`
/// `new RegExp()` -> `RegExp()`
pub fn substitute_global_new_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_global_new_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::NewExpression(e) = expr else { return };
let Expression::Identifier(ident) = &e.callee else { return };
let name = ident.name.as_str();
@@ -1216,7 +1231,10 @@ impl<'a> PeepholeOptimizations {
)
}
pub fn substitute_chain_call_expression(expr: &mut ChainExpression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_chain_call_expression(
expr: &mut ChainExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if let ChainElement::CallExpression(call_expr) = &mut expr.expression {
// `window.Object?.()` -> `Object?.()`
if call_expr.arguments.is_empty()
@@ -1237,7 +1255,7 @@ impl<'a> PeepholeOptimizations {
}
}
pub fn substitute_template_literal(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_template_literal(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::TemplateLiteral(t) = expr else { return };
let Some(val) = t.to_js_string(ctx) else { return };
*expr = ctx.ast.expression_string_literal(t.span(), ctx.ast.atom_from_cow(&val), None);
@@ -1248,7 +1266,7 @@ impl<'a> PeepholeOptimizations {
fn try_compress_property_key(
key: &mut PropertyKey<'a>,
computed: &mut bool,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
match key {
PropertyKey::NumericLiteral(_) => {
@@ -1258,7 +1276,7 @@ impl<'a> PeepholeOptimizations {
}
PropertyKey::StringLiteral(s) => {
let value = s.value.as_str();
if Ctx::is_identifier_name_patched(value) {
if TraverseCtx::is_identifier_name_patched(value) {
*computed = false;
*key = PropertyKey::StaticIdentifier(
ctx.ast.alloc_identifier_name(s.span, s.value),
@@ -1266,7 +1284,7 @@ impl<'a> PeepholeOptimizations {
ctx.state.changed = true;
return;
}
if let Some(value) = Ctx::string_to_equivalent_number_value(value)
if let Some(value) = TraverseCtx::string_to_equivalent_number_value(value)
&& value >= 0.0
{
*computed = false;
@@ -1289,7 +1307,7 @@ impl<'a> PeepholeOptimizations {
// `foo(...[1,2,3])` -> `foo(1,2,3)`
// `new Foo(...[1,2,3])` -> `new Foo(1,2,3)`
fn try_flatten_arguments(args: &mut Vec<'a, Argument<'a>>, ctx: &mut Ctx<'a, '_>) {
fn try_flatten_arguments(args: &mut Vec<'a, Argument<'a>>, ctx: &mut TraverseCtx<'a>) {
let (new_size, should_fold) =
args.iter().fold((0, false), |(mut new_size, mut should_fold), arg| {
new_size += if let Argument::SpreadElement(spread_el) = arg {
@@ -1346,7 +1364,10 @@ impl<'a> PeepholeOptimizations {
/// Flatten nested chain expressions
/// `(foo?.bar)?.baz` -> `foo?.bar?.baz`
fn try_flatten_nested_chain_expression(expr: &mut ChainExpression<'a>, ctx: &mut Ctx<'a, '_>) {
fn try_flatten_nested_chain_expression(
expr: &mut ChainExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
match &mut expr.expression {
ChainElement::StaticMemberExpression(member) => {
if let Expression::ChainExpression(chain) = member.object.without_parentheses_mut()
@@ -1395,7 +1416,7 @@ impl<'a> PeepholeOptimizations {
/// <https://tc39.es/ecma262/2025/multipage/fundamental-objects.html#sec-object-value>
fn try_rewrite_object_callee_indirect_call(
expr: &mut CallExpression<'a>,
ctx: &mut Ctx<'a, '_>,
ctx: &mut TraverseCtx<'a>,
) {
let Expression::CallExpression(inner_call) = &mut expr.callee else { return };
if inner_call.optional || inner_call.arguments.len() != 1 {
@@ -1429,7 +1450,7 @@ impl<'a> PeepholeOptimizations {
/// e.g. `var a = function f() {}` -> `var a = function () {}`
///
/// This compression is not safe if the code relies on `Function::name`.
pub fn try_remove_name_from_functions(func: &mut Function<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_remove_name_from_functions(func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) {
if ctx.options().keep_names.function {
return;
}
@@ -1444,7 +1465,7 @@ impl<'a> PeepholeOptimizations {
/// e.g. `var a = class C {}` -> `var a = class {}`
///
/// This compression is not safe if the code relies on `Class::name`.
pub fn try_remove_name_from_classes(class: &mut Class<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn try_remove_name_from_classes(class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
if ctx.options().keep_names.class {
return;
}
@@ -1456,7 +1477,7 @@ impl<'a> PeepholeOptimizations {
}
/// `new Int8Array(0)` -> `new Int8Array()` (also for other TypedArrays)
pub fn substitute_typed_array_constructor(e: &mut NewExpression<'a>, ctx: &Ctx<'a, '_>) {
pub fn substitute_typed_array_constructor(e: &mut NewExpression<'a>, ctx: &TraverseCtx<'a>) {
let Expression::Identifier(ident) = &e.callee else { return };
let name = ident.name.as_str();
if !Self::is_typed_array_name(name) || !ctx.is_global_reference(ident) {
@@ -1470,7 +1491,7 @@ impl<'a> PeepholeOptimizations {
}
/// Transforms boolean expression `true` => `!0` `false` => `!1`.
pub fn substitute_boolean(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_boolean(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BooleanLiteral(lit) = expr else { return };
let num = ctx.ast.expression_numeric_literal(
lit.span,
@@ -1483,7 +1504,7 @@ impl<'a> PeepholeOptimizations {
}
/// Transforms long array expression with string literals to `"str1,str2".split(',')`
pub fn substitute_array_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_array_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
// this threshold is chosen by hand by checking the minsize output
const THRESHOLD: usize = 40;
@@ -1554,7 +1575,7 @@ impl<'a> PeepholeOptimizations {
DELIMITERS.into_iter().find(|&delimiter| strings.clone().all(|s| !s.contains(delimiter)))
}
pub fn substitute_catch_clause(catch: &mut CatchClause<'a>, ctx: &Ctx<'a, '_>) {
pub fn substitute_catch_clause(catch: &mut CatchClause<'a>, ctx: &TraverseCtx<'a>) {
if ctx.supports_feature(ESFeature::ES2019OptionalCatchBinding)
&& let Some(param) = &catch.param
&& let BindingPattern::BindingIdentifier(ident) = &param.pattern
@@ -1585,7 +1606,7 @@ impl<'a> PeepholeOptimizations {
}
/// Checks if the expression result is unused (i.e., in an expression statement context).
fn is_expression_result_unused(ctx: &Ctx<'a, '_>) -> bool {
fn is_expression_result_unused(ctx: &TraverseCtx<'a>) -> bool {
matches!(ctx.parent(), Ancestor::ExpressionStatementExpression(_))
}
@@ -1597,7 +1618,7 @@ impl<'a> PeepholeOptimizations {
/// - Simplifies single-expression non-async arrow function IIFEs (e.g., `(() => foo())()` to `foo()`).
/// - Converts arrow function IIFEs that return void or execute one expression
/// (e.g., `(() => { foo() })()` or `(() => { return foo() })()`) into simpler expressions.
pub fn substitute_iife_call(e: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
pub fn substitute_iife_call(e: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(call_expr) = e else { return };
if !call_expr.arguments.is_empty() || !call_expr.callee.is_function() {
-1
View File
@@ -15,7 +15,6 @@ pub struct SymbolValue<'a> {
pub read_references_count: u32,
pub write_references_count: u32,
#[expect(unused)]
pub scope_id: ScopeId,
}
@@ -0,0 +1,207 @@
use std::mem::transmute;
use oxc_data_structures::stack::NonEmptyStack;
use crate::generated::ancestor::{Ancestor, AncestorType};
const INITIAL_STACK_CAPACITY: usize = 64; // 64 entries = 1 KiB
/// Traverse ancestry context.
///
/// Contains a stack of `Ancestor`s, and provides methods to get parent/ancestor of current node.
///
/// `walk_*` methods push/pop `Ancestor`s to `stack` when entering/exiting nodes.
///
/// `Ancestor<'a, 't>` is an owned type.
/// * `'a` is lifetime of AST nodes.
/// * `'t` is lifetime of the `Ancestor` (derived from `&'t TraverseAncestry`).
///
/// `'t` is constrained in `parent`, `ancestor` and `ancestors` methods to only live as long as
/// the `&'t TraverseAncestry` passed to the method.
/// i.e. `Ancestor`s can only live as long as `enter_*` or `exit_*` method in which they're obtained,
/// and cannot "escape" those methods.
/// This is required for soundness. If an `Ancestor` could be retained longer, the references that
/// can be got from it could alias a `&mut` reference to the same AST node.
///
/// # SAFETY
/// This type MUST NOT be mutable by consumer.
///
/// The safety scheme is entirely reliant on `stack` being in sync with the traversal,
/// to prevent consumer from accessing fields of nodes which traversal has passed through,
/// so as to not violate Rust's aliasing rules.
/// If consumer could alter `stack` in any way, they could break the safety invariants and cause UB.
///
/// We prevent this in 3 ways:
/// 1. `TraverseAncestry`'s `stack` field is private.
/// 2. Public methods of `TraverseAncestry` provide no means for mutating `stack`.
/// 3. Visitors receive a `&mut TraverseCtx`, but cannot overwrite its `ancestry` field because they:
/// a. cannot create a new `TraverseAncestry`
/// - `TraverseAncestry::new` and `TraverseCtx::new` are private.
/// b. cannot obtain an owned `TraverseAncestry` from a `&TraverseAncestry`
/// - `TraverseAncestry` is not `Clone`.
///
/// ## Soundness hole
///
/// Strictly speaking, there is still room to abuse the API and cause UB as follows:
///
/// * Initiate a 2nd traversal of a different AST inside a `Traverse` visitor method.
/// * `mem::swap` the 2 x `&mut TraverseCtx`s from the 2 different traversals.
///
/// The 2 ASTs would have to be different, but borrowed for same lifetime, so I (@overlookmotel) don't
/// think it's possible by this method to produce aliasing violations, or to over-extend AST node
/// lifetimes to cause a use-after-free.
/// But it *could* produce buffer underrun in `pop_stack`, when it tries to pop from a stack which
/// is already empty.
///
/// In practice, this would be a completely bizarre thing to do, and would basically require you to
/// write malicious code specifically designed to cause UB. So it's not a particularly real risk.
///
/// To close this hole and make the API 100% sound, we'd need branded lifetimes so that all
/// `TraverseCtx`s have unique lifetimes, and so cannot be swapped for any other without
/// the borrow-checker complaining.
pub struct TraverseAncestry<'a> {
stack: NonEmptyStack<Ancestor<'a, 'static>>,
}
// Public methods
impl<'a> TraverseAncestry<'a> {
/// Get parent of current node.
#[inline]
pub fn parent<'t>(&'t self) -> Ancestor<'a, 't> {
let ancestor = *self.stack.last();
// Shrink `Ancestor`'s `'t` lifetime to lifetime of `&'t self`.
// SAFETY: The `Ancestor` is guaranteed valid for `'t`. It is not possible to obtain
// a `&mut` ref to any AST node which this `Ancestor` gives access to during `'t`.
unsafe { transmute::<Ancestor<'a, '_>, Ancestor<'a, 't>>(ancestor) }
}
/// Get ancestor of current node.
///
/// `level` is number of levels above parent.
/// `ancestor(0)` is equivalent to `parent()` (but better to use `parent()` as it's more efficient).
///
/// If `level` is out of bounds (above `Program`), returns `Ancestor::None`.
#[inline]
pub fn ancestor<'t>(&'t self, level: usize) -> Ancestor<'a, 't> {
// Behavior with different values:
// `len = 1, level = 0` -> return `Ancestor::None` from else branch
// `len = 1, level = 1` -> return `Ancestor::None` from else branch (out of bounds)
// `len = 3, level = 0` -> return parent (index 2)
// `len = 3, level = 1` -> return grandparent (index 1)
// `len = 3, level = 2` -> return `Ancestor::None` from else branch
// `len = 3, level = 3` -> return `Ancestor::None` from else branch (out of bounds)
// `self.stack.len()` is always at least 1, so `self.stack.len() - 1` cannot wrap around.
// `level <= last_index` would also work here, but `level < last_index` avoids a read from memory
// when that read would just get `Ancestor::None` anyway.
let last_index = self.stack.len() - 1;
if level < last_index {
// SAFETY: We just checked that `level < last_index` so `last_index - level` cannot wrap around,
// and `last_index - level` must be a valid index
let ancestor = unsafe { *self.stack.get_unchecked(last_index - level) };
// Shrink `Ancestor`'s `'t` lifetime to lifetime of `&'t self`.
// SAFETY: The `Ancestor` is guaranteed valid for `'t`. It is not possible to obtain
// a `&mut` ref to any AST node which this `Ancestor` gives access to during `'t`.
unsafe { transmute::<Ancestor<'a, '_>, Ancestor<'a, 't>>(ancestor) }
} else {
Ancestor::None
}
}
/// Get iterator over ancestors, starting with parent and working up.
///
/// Last `Ancestor` returned will be `Program`. `Ancestor::None` is not included in iteration.
pub fn ancestors<'t>(&'t self) -> impl Iterator<Item = Ancestor<'a, 't>> {
// SAFETY: Stack always has at least 1 entry
let stack_without_first = unsafe { self.stack.get_unchecked(1..) };
stack_without_first.iter().rev().map(|&ancestor| {
// Shrink `Ancestor`'s `'t` lifetime to lifetime of `&'t self`.
// SAFETY: The `Ancestor` is guaranteed valid for `'t`. It is not possible to obtain
// a `&mut` ref to any AST node which this `Ancestor` gives access to during `'t`.
unsafe { transmute::<Ancestor<'a, '_>, Ancestor<'a, 't>>(ancestor) }
})
}
/// Get depth in the AST.
///
/// Count includes current node. i.e. in `Program`, depth is 1.
#[inline]
pub fn ancestors_depth(&self) -> usize {
self.stack.len()
}
}
// Methods used internally within crate.
impl<'a> TraverseAncestry<'a> {
/// Create new `TraverseAncestry`.
///
/// # SAFETY
/// This method must not be public outside this crate, or consumer could break safety invariants.
pub(super) fn new() -> Self {
Self { stack: NonEmptyStack::with_capacity(INITIAL_STACK_CAPACITY, Ancestor::None) }
}
/// Push item onto ancestry stack.
///
/// # SAFETY
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
#[must_use] // `PopToken` must be passed to `pop_stack` to pop this entry off the stack again
pub(crate) fn push_stack(&mut self, ancestor: Ancestor<'a, 'static>) -> PopToken {
self.stack.push(ancestor);
// Return `PopToken` which can be used to pop this entry off again
PopToken(())
}
/// Pop last item off ancestry stack.
///
/// # SAFETY
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
#[expect(unused_variables, clippy::needless_pass_by_value)]
pub(crate) fn pop_stack(&mut self, token: PopToken) {
// SAFETY: `PopToken`s are only created in `push_stack`, so the fact that caller provides one
// guarantees that a push has happened. This method consumes the token which guarantees another
// pop hasn't occurred already corresponding to that push.
// Therefore the stack cannot by empty.
// The stack starts with 1 entry, so also it cannot be left empty after this pop.
unsafe { self.stack.pop_unchecked() };
}
/// Retag last item on ancestry stack.
///
/// i.e. Alter discriminant of `Ancestor` enum, without changing the "payload" it contains
/// of pointer to the ancestor node.
///
/// This is purely a performance optimization. If the last item on stack already contains the
/// correct pointer, then `ctx.retag_stack(AncestorType::ProgramBody)` is equivalent to:
///
/// ```nocompile
/// ctx.pop_stack();
/// ctx.push_stack(Ancestor::ProgramBody(ProgramWithoutBody(node_ptr)));
/// ```
///
/// `retag_stack` is only a single 2-byte write operation.
///
/// # SAFETY
/// * Stack must have length of at least 2 (so we are not retagging dummy root `Ancestor`).
/// * Last item on stack must contain pointer to type corresponding to provided `AncestorType`.
///
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
#[expect(clippy::ptr_as_ptr, clippy::ref_as_ptr)]
pub(crate) unsafe fn retag_stack(&mut self, ty: AncestorType) {
debug_assert!(self.stack.len() >= 2);
// SAFETY: Caller must uphold the safety invariants
unsafe { *(self.stack.last_mut() as *mut _ as *mut AncestorType) = ty };
}
}
/// Zero sized token which allows popping from stack. Used to ensure push and pop always correspond.
/// Inner field is private to this module so can only be created by methods in this file.
/// It is not `Clone` or `Copy`, so no way to obtain one except in this file.
/// Only method which generates a `PopToken` is `push_stack`, and `pop_stack` consumes one,
/// which guarantees you can't have more pops than pushes.
pub struct PopToken(());
@@ -0,0 +1,345 @@
use oxc_ast::ast::{
AssignmentTarget, BindingIdentifier, BindingPattern, Expression, IdentifierReference,
SimpleAssignmentTarget,
};
use oxc_span::{SPAN, Span};
use oxc_str::Ident;
use oxc_syntax::{reference::ReferenceFlags, symbol::SymbolId};
use super::TraverseCtx;
use super::MaybeBoundIdentifier;
/// Info about a binding, from which one can create a `BindingIdentifier` or `IdentifierReference`s.
///
/// Typical usage:
///
/// ```rs
/// // Generate a UID for a top-level var
/// let binding = ctx.generate_uid_in_current_scope("foo", SymbolFlags::FunctionScopedVariable);
///
/// // Generate `IdentifierReference`s and insert them into AST
/// some_node.id = binding.create_read_reference(ctx);
/// some_other_node.id = binding.create_read_reference(ctx);
///
/// // Store details of the binding for later
/// self.foo_binding = binding;
///
/// // Later on in `exit_program`
/// let id = self.foo_binding.create_binding_identifier(ctx);
/// // Insert `var <id> = something;` into `program.body`
/// ```
///
/// Notes:
///
/// * `BoundIdentifier` is smaller than `BindingIdentifier`, so takes less memory when you store
/// it for later use.
/// * `BoundIdentifier` is `Clone` (unlike `BindingIdentifier`).
/// * `BoundIdentifier` re-uses the same `Ident` for all `BindingIdentifier` / `IdentifierReference`s
/// created from it.
#[derive(Debug, Clone)]
pub struct BoundIdentifier<'a> {
pub name: Ident<'a>,
pub symbol_id: SymbolId,
}
impl<'a> BoundIdentifier<'a> {
/// Create `BoundIdentifier` for `name` and `symbol_id`
pub fn new(name: Ident<'a>, symbol_id: SymbolId) -> Self {
Self { name, symbol_id }
}
/// Create `BoundIdentifier` from a `BindingIdentifier`
pub fn from_binding_ident(ident: &BindingIdentifier<'a>) -> Self {
Self { name: ident.name, symbol_id: ident.symbol_id() }
}
/// Convert `BoundIdentifier` to `MaybeBoundIdentifier`
pub fn to_maybe_bound_identifier(&self) -> MaybeBoundIdentifier<'a> {
MaybeBoundIdentifier::new(self.name, Some(self.symbol_id))
}
/// Create `BindingIdentifier` for this binding
pub fn create_binding_identifier<State>(
&self,
ctx: &TraverseCtx<'a, State>,
) -> BindingIdentifier<'a> {
ctx.ast.binding_identifier_with_symbol_id(SPAN, self.name, self.symbol_id)
}
/// Create `BindingPattern` for this binding
pub fn create_binding_pattern<State>(
&self,
ctx: &TraverseCtx<'a, State>,
) -> BindingPattern<'a> {
let ident = self.create_binding_identifier(ctx);
BindingPattern::BindingIdentifier(ctx.alloc(ident))
}
// --- Read only ---
/// Create `IdentifierReference` referencing this binding, which is read from, with dummy `Span`
pub fn create_read_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_read_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from, with dummy `Span`
pub fn create_read_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_read_expression(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is read from, with specified `Span`
pub fn create_spanned_read_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Read, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from, with specified `Span`
pub fn create_spanned_read_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Read, ctx)
}
// --- Write only ---
/// Create `IdentifierReference` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_write_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_write_expression(SPAN, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_write_target(SPAN, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_simple_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_write_simple_target(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Write, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Write, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(span, ReferenceFlags::Write, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_simple_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(span, ReferenceFlags::Write, ctx)
}
// --- Read and write ---
/// Create `IdentifierReference` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_read_write_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_read_write_expression(SPAN, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_read_write_target(SPAN, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_simple_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_read_write_simple_target(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_simple_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
// --- Specified ReferenceFlags ---
/// Create `IdentifierReference` referencing this binding, with specified `ReferenceFlags`
pub fn create_reference<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(SPAN, flags, ctx)
}
/// Create `Expression::Identifier` referencing this binding, with specified `ReferenceFlags`
pub fn create_expression<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(SPAN, flags, ctx)
}
/// Create `AssignmentTarget` referencing this binding, with specified `ReferenceFlags`
pub fn create_target<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(SPAN, flags, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, with specified `ReferenceFlags`
pub fn create_simple_target<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(SPAN, flags, ctx)
}
/// Create `IdentifierReference` referencing this binding, with specified `Span` and `ReferenceFlags`
pub fn create_spanned_reference<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
ctx.create_bound_ident_reference(span, self.name, self.symbol_id, flags)
}
/// Create `Expression::Identifier` referencing this binding, with specified `Span` and `ReferenceFlags`
pub fn create_spanned_expression<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
Expression::Identifier(ctx.alloc(ident))
}
/// Create `AssignmentTarget::AssignmentTargetIdentifier` referencing this binding,
/// with specified `Span` and `ReferenceFlags`
pub fn create_spanned_target<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
AssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(ident))
}
/// Create `SimpleAssignmentTarget::AssignmentTargetIdentifier` referencing this binding,
/// with specified `Span` and `ReferenceFlags`
pub fn create_spanned_simple_target<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
SimpleAssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(ident))
}
}
@@ -1,51 +1,36 @@
use std::ops::{Deref, DerefMut};
use oxc_ast::{AstBuilder, ast::*};
use oxc_compat::ESFeature;
use oxc_ecmascript::{
GlobalContext,
constant_evaluation::{
ConstantEvaluation, ConstantEvaluationCtx, ConstantValue, binary_operation_evaluate_value,
},
side_effects::{MayHaveSideEffects, PropertyReadSideEffects, is_pure_function},
side_effects::{
MayHaveSideEffects, MayHaveSideEffectsContext, PropertyReadSideEffects, is_pure_function,
},
};
use oxc_semantic::{IsGlobalReference, Scoping, SymbolId};
use oxc_semantic::{IsGlobalReference, SymbolId};
use oxc_span::format_atom;
use oxc_syntax::{
identifier::{is_identifier_part, is_identifier_start},
reference::ReferenceId,
scope::ScopeFlags,
};
use oxc_traverse::Ancestor;
use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue};
use oxc_compat::ESFeature;
use crate::{
generated::ancestor::Ancestor, options::CompressOptions, state::MinifierState,
symbol_value::SymbolValue,
};
pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;
use super::TraverseCtx;
pub struct Ctx<'a, 'b>(&'b mut TraverseCtx<'a>);
impl<'a, 'b> Ctx<'a, 'b> {
pub fn new(ctx: &'b mut TraverseCtx<'a>) -> Self {
Self(ctx)
}
pub fn is_exact_int64(num: f64) -> bool {
num.fract() == 0.0
}
impl<'a> Deref for Ctx<'a, '_> {
type Target = TraverseCtx<'a>;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<'a> DerefMut for Ctx<'a, '_> {
fn deref_mut(&mut self) -> &mut TraverseCtx<'a> {
self.0
}
}
impl<'a> oxc_ecmascript::GlobalContext<'a> for Ctx<'a, '_> {
impl<'a> GlobalContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
ident.is_global_reference(self.0.scoping())
ident.is_global_reference(self.scoping())
}
fn get_constant_value_for_reference_id(
@@ -62,7 +47,33 @@ impl<'a> oxc_ecmascript::GlobalContext<'a> for Ctx<'a, '_> {
}
}
impl<'a> oxc_ecmascript::side_effects::MayHaveSideEffectsContext<'a> for Ctx<'a, '_> {
impl<'a> GlobalContext<'a> for &TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
(*self).is_global_reference(ident)
}
fn get_constant_value_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<ConstantValue<'a>> {
(*self).get_constant_value_for_reference_id(reference_id)
}
}
impl<'a> GlobalContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
(**self).is_global_reference(ident)
}
fn get_constant_value_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<ConstantValue<'a>> {
(**self).get_constant_value_for_reference_id(reference_id)
}
}
impl<'a> MayHaveSideEffectsContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
self.state.options.treeshake.annotations
}
@@ -84,23 +95,63 @@ impl<'a> oxc_ecmascript::side_effects::MayHaveSideEffectsContext<'a> for Ctx<'a,
}
}
impl<'a> ConstantEvaluationCtx<'a> for Ctx<'a, '_> {
impl<'a> MayHaveSideEffectsContext<'a> for &TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
(*self).annotations()
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
(*self).manual_pure_functions(callee)
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
(*self).property_read_side_effects()
}
fn unknown_global_side_effects(&self) -> bool {
(*self).unknown_global_side_effects()
}
}
impl<'a> MayHaveSideEffectsContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
(**self).annotations()
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
(**self).manual_pure_functions(callee)
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
(**self).property_read_side_effects()
}
fn unknown_global_side_effects(&self) -> bool {
(**self).unknown_global_side_effects()
}
}
impl<'a> ConstantEvaluationCtx<'a> for TraverseCtx<'a, MinifierState<'a>> {
fn ast(&self) -> AstBuilder<'a> {
self.ast
}
}
pub fn is_exact_int64(num: f64) -> bool {
num.fract() == 0.0
impl<'a> ConstantEvaluationCtx<'a> for &TraverseCtx<'a, MinifierState<'a>> {
fn ast(&self) -> AstBuilder<'a> {
(*self).ast()
}
}
impl<'a> Ctx<'a, '_> {
pub fn scoping(&self) -> &Scoping {
self.0.scoping()
impl<'a> ConstantEvaluationCtx<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
fn ast(&self) -> AstBuilder<'a> {
(**self).ast()
}
}
impl<'a> TraverseCtx<'a, MinifierState<'a>> {
pub fn options(&self) -> &CompressOptions {
&self.0.state.options
&self.state.options
}
/// Check if the target engines supports a feature.
@@ -111,11 +162,11 @@ impl<'a> Ctx<'a, '_> {
}
pub fn source_type(&self) -> SourceType {
self.0.state.source_type
self.state.source_type
}
pub fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool {
ident.is_global_reference(self.0.scoping())
ident.is_global_reference(self.scoping())
}
pub fn eval_binary(&self, e: &BinaryExpression<'a>) -> Option<Expression<'a>> {
@@ -0,0 +1,325 @@
use oxc_ast::ast::{AssignmentTarget, Expression, IdentifierReference, SimpleAssignmentTarget};
use oxc_span::{SPAN, Span};
use oxc_str::Ident;
use oxc_syntax::{reference::ReferenceFlags, symbol::SymbolId};
use super::TraverseCtx;
use super::BoundIdentifier;
/// A factory for generating `IdentifierReference`s.
///
/// Typical usage:
///
/// ```rs
/// // Create `MaybeBoundIdentifier` from an existing `IdentifierReference`
/// let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx);
///
/// // Generate `IdentifierReference`s and insert them into AST
/// assign_expr.left = binding.create_write_target(ctx);
/// assign_expr.right = binding.create_read_expression(ctx);
/// ```
///
/// Notes:
///
/// * The original `IdentifierReference` must also be used in the AST, or it'll be a dangling reference.
/// * `MaybeBoundIdentifier` is smaller than `IdentifierReference`, so takes less memory when you store
/// it for later use.
/// * `MaybeBoundIdentifier` re-uses the same `Ident` for all `BindingIdentifier` / `IdentifierReference`s
/// created from it.
/// * `MaybeBoundIdentifier` looks up the `SymbolId` for the reference only once.
#[derive(Debug, Clone)]
pub struct MaybeBoundIdentifier<'a> {
pub name: Ident<'a>,
pub symbol_id: Option<SymbolId>,
}
impl<'a> MaybeBoundIdentifier<'a> {
/// Create `MaybeBoundIdentifier` for `name` and `Option<SymbolId>`
pub fn new(name: Ident<'a>, symbol_id: Option<SymbolId>) -> Self {
Self { name, symbol_id }
}
/// Create `MaybeBoundIdentifier` from an `IdentifierReference`
pub fn from_identifier_reference<State>(
ident: &IdentifierReference<'a>,
ctx: &TraverseCtx<'a, State>,
) -> Self {
let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id();
Self { name: ident.name, symbol_id }
}
/// Convert `MaybeBoundIdentifier` to `BoundIdentifier`.
///
/// Returns `None` if symbol is not bound.
pub fn to_bound_identifier(&self) -> Option<BoundIdentifier<'a>> {
self.symbol_id.map(|symbol_id| BoundIdentifier::new(self.name, symbol_id))
}
// --- Read only ---
/// Create `IdentifierReference` referencing this binding, which is read from, with dummy `Span`
pub fn create_read_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_read_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from, with dummy `Span`
pub fn create_read_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_read_expression(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is read from, with specified `Span`
pub fn create_spanned_read_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Read, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from, with specified `Span`
pub fn create_spanned_read_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Read, ctx)
}
// --- Write only ---
/// Create `IdentifierReference` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_write_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_write_expression(SPAN, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_write_target(SPAN, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is written to, with dummy `Span`
pub fn create_write_simple_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_write_simple_target(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Write, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Write, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(span, ReferenceFlags::Write, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is written to, with specified `Span`
pub fn create_spanned_write_simple_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(span, ReferenceFlags::Write, ctx)
}
// --- Read and write ---
/// Create `IdentifierReference` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_reference<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_read_write_reference(SPAN, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_expression<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_read_write_expression(SPAN, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_read_write_target(SPAN, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is read from + written to,
/// with dummy `Span`
pub fn create_read_write_simple_target<State>(
&self,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_read_write_simple_target(SPAN, ctx)
}
/// Create `IdentifierReference` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_reference<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `Expression::Identifier` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_expression<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `AssignmentTarget` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, which is read from + written to,
/// with specified `Span`
pub fn create_spanned_read_write_simple_target<State>(
&self,
span: Span,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(span, ReferenceFlags::Read | ReferenceFlags::Write, ctx)
}
// --- Specified ReferenceFlags ---
/// Create `IdentifierReference` referencing this binding, with specified `ReferenceFlags`
pub fn create_reference<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
self.create_spanned_reference(SPAN, flags, ctx)
}
/// Create `Expression::Identifier` referencing this binding, with specified `ReferenceFlags`
pub fn create_expression<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
self.create_spanned_expression(SPAN, flags, ctx)
}
/// Create `AssignmentTarget` referencing this binding, with specified `ReferenceFlags`
pub fn create_target<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
self.create_spanned_target(SPAN, flags, ctx)
}
/// Create `SimpleAssignmentTarget` referencing this binding, with specified `ReferenceFlags`
pub fn create_simple_target<State>(
&self,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
self.create_spanned_simple_target(SPAN, flags, ctx)
}
/// Create `IdentifierReference` referencing this binding, with specified `Span` and `ReferenceFlags`
pub fn create_spanned_reference<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> IdentifierReference<'a> {
ctx.create_ident_reference(span, self.name, self.symbol_id, flags)
}
/// Create `Expression::Identifier` referencing this binding, with specified `Span` and `ReferenceFlags`
pub fn create_spanned_expression<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> Expression<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
Expression::Identifier(ctx.alloc(ident))
}
/// Create `AssignmentTarget::AssignmentTargetIdentifier` referencing this binding,
/// with specified `Span` and `ReferenceFlags`
pub fn create_spanned_target<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> AssignmentTarget<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
AssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(ident))
}
/// Create `SimpleAssignmentTarget::AssignmentTargetIdentifier` referencing this binding,
/// with specified `Span` and `ReferenceFlags`
pub fn create_spanned_simple_target<State>(
&self,
span: Span,
flags: ReferenceFlags,
ctx: &mut TraverseCtx<'a, State>,
) -> SimpleAssignmentTarget<'a> {
let ident = self.create_spanned_reference(span, flags, ctx);
SimpleAssignmentTarget::AssignmentTargetIdentifier(ctx.alloc(ident))
}
}
@@ -0,0 +1,717 @@
use oxc_allocator::{Allocator, Box as ArenaBox, Vec as ArenaVec};
use oxc_ast::{
AstBuilder,
ast::{Expression, IdentifierReference, Statement},
};
use oxc_semantic::Scoping;
use oxc_span::Span;
use oxc_str::Ident;
use oxc_syntax::{
reference::{ReferenceFlags, ReferenceId},
scope::{ScopeFlags, ScopeId},
symbol::{SymbolFlags, SymbolId},
};
use oxc_traverse::ast_operations::{GatherNodeParts, get_var_name_from_node};
use crate::{
generated::ancestor::{Ancestor, AncestorType},
state::MinifierState,
};
mod ancestry;
mod bound_identifier;
mod ecma_context;
mod maybe_bound_identifier;
mod reusable;
mod scopes_collector;
mod scoping;
mod uid;
use ancestry::PopToken;
pub use ancestry::TraverseAncestry;
pub use bound_identifier::BoundIdentifier;
pub use maybe_bound_identifier::MaybeBoundIdentifier;
pub use reusable::ReusableTraverseCtx;
pub use scoping::TraverseScoping;
pub type MinifierTraverseCtx<'a> = TraverseCtx<'a, MinifierState<'a>>;
pub type ReusableMinifierTraverseCtx<'a> = ReusableTraverseCtx<'a, MinifierState<'a>>;
/// Traverse context.
///
/// Passed to all AST visitor functions.
///
/// Provides ability to:
/// * Query parent/ancestor of current node via [`parent`], [`ancestor`], [`ancestors`].
/// * Get scopes tree and symbols table via [`scoping`] and [`scoping_mut`],
/// [`ancestor_scopes`].
/// * Create AST nodes via AST builder [`ast`].
/// * Allocate into arena via [`alloc`].
///
/// # Namespaced APIs
///
/// All APIs are provided via 2 routes:
///
/// 1. Directly on `TraverseCtx`.
/// 2. Via "namespaces".
///
/// | Direct | Namespaced |
/// |--------------------------|----------------------------------|
/// | `ctx.parent()` | `ctx.ancestry.parent()` |
/// | `ctx.current_scope_id()` | `ctx.scoping.current_scope_id()` |
/// | `ctx.alloc(thing)` | `ctx.ast.alloc(thing)` |
///
/// Purpose of the "namespaces" is to support if you want to mutate scope tree or symbol table
/// while holding an `&Ancestor`, or AST nodes obtained from an `&Ancestor`.
///
/// For example, this will not compile because it attempts to borrow `ctx`
/// immutably and mutably at same time:
///
/// ```nocompile
/// use oxc_ast::ast::*;
/// use oxc_traverse::{Ancestor, Traverse, TraverseCtx};
///
/// struct MyTransform;
/// impl<'a> Traverse<'a> for MyTransform {
/// fn enter_unary_expression(&mut self, unary_expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a>) {
/// // `right` is ultimately borrowed from `ctx`
/// let right = match ctx.parent() {
/// Ancestor::BinaryExpressionLeft(bin_expr) => bin_expr.right(),
/// _ => return,
/// };
///
/// // Won't compile! `ctx.scopes_mut()` attempts to mut borrow `ctx`
/// // while it's already borrowed by `right`.
/// let scope_tree_mut = ctx.scopes_mut();
///
/// // Use `right` later on
/// dbg!(right);
/// }
/// }
/// ```
///
/// You can fix this by using the "namespaced" methods instead.
/// This works because you can borrow `ctx.ancestry` and `ctx.scoping` simultaneously:
///
/// ```
/// use oxc_ast::ast::*;
/// use oxc_traverse::{Ancestor, Traverse, TraverseCtx};
///
/// struct MyTransform;
/// impl<'a> Traverse<'a, ()> for MyTransform {
/// fn enter_unary_expression(&mut self, unary_expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a, ()>) {
/// let right = match ctx.ancestry.parent() {
/// Ancestor::BinaryExpressionLeft(bin_expr) => bin_expr.right(),
/// _ => return,
/// };
///
/// let scoping_mut = ctx.scoping.scoping_mut();
///
/// dbg!(right);
/// }
/// }
/// ```
///
/// [`parent`]: `TraverseCtx::parent`
/// [`ancestor`]: `TraverseCtx::ancestor`
/// [`ancestors`]: `TraverseCtx::ancestors`
/// [`scoping`]: `TraverseCtx::scoping`
/// [`scoping_mut`]: `TraverseCtx::scoping_mut`
/// [`ancestor_scopes`]: `TraverseCtx::ancestor_scopes`
/// [`ast`]: `TraverseCtx::ast`
/// [`alloc`]: `TraverseCtx::alloc`
pub struct TraverseCtx<'a, State> {
pub state: State,
pub ancestry: TraverseAncestry<'a>,
pub scoping: TraverseScoping<'a>,
pub ast: AstBuilder<'a>,
}
// Public methods
impl<'a, State> TraverseCtx<'a, State> {
/// Allocate a node in the arena.
///
/// Returns a [`Box<'a, T>`](ArenaBox).
///
/// Shortcut for `ctx.ast.alloc`.
#[inline]
pub fn alloc<T>(&self, node: T) -> ArenaBox<'a, T> {
self.ast.alloc(node)
}
/// Get parent of current node.
///
/// Shortcut for `ctx.ancestry.parent`.
#[inline]
pub fn parent<'t>(&'t self) -> Ancestor<'a, 't> {
self.ancestry.parent()
}
/// Get ancestor of current node.
///
/// `level` is number of levels above parent.
/// `ancestor(0)` is equivalent to `parent()` (but better to use `parent()` as it's more efficient).
///
/// If `level` is out of bounds (above `Program`), returns `Ancestor::None`.
///
/// Shortcut for `ctx.ancestry.ancestor`.
#[inline]
pub fn ancestor<'t>(&'t self, level: usize) -> Ancestor<'a, 't> {
self.ancestry.ancestor(level)
}
/// Get iterator over ancestors, starting with parent and working up.
///
/// Last `Ancestor` returned will be `Program`. `Ancestor::None` is not included in iteration.
///
/// Shortcut for `ctx.ancestry.ancestors`.
#[inline]
pub fn ancestors<'t>(&'t self) -> impl Iterator<Item = Ancestor<'a, 't>> {
self.ancestry.ancestors()
}
/// Get depth in the AST.
///
/// Count includes current node. i.e. in `Program`, depth is 1.
///
/// Shortcut for `self.ancestry.ancestors_depth`.
#[inline]
pub fn ancestors_depth(&self) -> usize {
self.ancestry.ancestors_depth()
}
/// Get current scope ID.
///
/// Shortcut for `ctx.scoping.current_scope_id`.
#[inline]
pub fn current_scope_id(&self) -> ScopeId {
self.scoping.current_scope_id()
}
/// Get current var hoisting scope ID.
///
/// Shortcut for `ctx.scoping.current_hoist_scope_id`.
#[inline]
pub fn current_hoist_scope_id(&self) -> ScopeId {
self.scoping.current_hoist_scope_id()
}
/// Get current block scope ID.
///
/// Shortcut for `ctx.scoping.current_block_scope_id`.
#[inline]
pub fn current_block_scope_id(&self) -> ScopeId {
self.scoping.current_block_scope_id()
}
/// Get current scope flags.
///
/// Shortcut for `ctx.scoping.current_scope_flags`.
#[inline]
pub fn current_scope_flags(&self) -> ScopeFlags {
self.scoping.current_scope_flags()
}
/// Get scopes tree.
///
/// Shortcut for `ctx.scoping.scopes`.
#[inline]
pub fn scoping(&self) -> &Scoping {
self.scoping.scoping()
}
/// Get mutable scopes tree.
///
/// Shortcut for `ctx.scoping.scopes_mut`.
#[inline]
pub fn scoping_mut(&mut self) -> &mut Scoping {
self.scoping.scoping_mut()
}
/// Get iterator over scopes, starting with current scope and working up.
///
/// This is a shortcut for `ctx.scoping.parent_scopes`.
#[inline]
pub fn ancestor_scopes(&self) -> impl Iterator<Item = ScopeId> + '_ {
self.scoping.ancestor_scopes()
}
/// Create new scope as child of provided scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.create_child_scope`.
#[inline]
pub fn create_child_scope(&mut self, parent_id: ScopeId, flags: ScopeFlags) -> ScopeId {
self.scoping.create_child_scope(parent_id, flags)
}
/// Create new scope as child of current scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.create_child_scope_of_current`.
#[inline]
pub fn create_child_scope_of_current(&mut self, flags: ScopeFlags) -> ScopeId {
self.scoping.create_child_scope_of_current(flags)
}
/// Insert a scope into scope tree below a statement.
///
/// Statement must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.insert_scope_below_statement`.
#[inline]
pub fn insert_scope_below_statement(&mut self, stmt: &Statement, flags: ScopeFlags) -> ScopeId {
self.scoping.insert_scope_below_statement(stmt, flags)
}
/// Insert a scope into scope tree below a statement.
///
/// Statement must be in provided scope.
/// New scope is created as child of the provided scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.insert_scope_below_statement_from_scope_id`.
#[inline]
pub fn insert_scope_below_statement_from_scope_id(
&mut self,
stmt: &Statement,
scope_id: ScopeId,
flags: ScopeFlags,
) -> ScopeId {
self.scoping.insert_scope_below_statement_from_scope_id(stmt, scope_id, flags)
}
/// Insert a scope into scope tree below an expression.
///
/// Expression must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the expression are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.insert_scope_below_expression`.
#[inline]
pub fn insert_scope_below_expression(
&mut self,
expr: &Expression,
flags: ScopeFlags,
) -> ScopeId {
self.scoping.insert_scope_below_expression(expr, flags)
}
/// Insert a scope into scope tree below a `Vec` of statements.
///
/// Statements must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
///
/// This is a shortcut for `ctx.scoping.insert_scope_below_statements`.
pub fn insert_scope_below_statements(
&mut self,
stmts: &ArenaVec<Statement>,
flags: ScopeFlags,
) -> ScopeId {
self.scoping.insert_scope_below_statements(stmts, flags)
}
/// Insert a scope between a parent and a child scope.
///
/// For example, given the following scopes
/// ```ts
/// parentScope1: {
/// childScope: { }
/// childScope2: { }
/// }
/// ```
/// and calling this function with `parentScope1` and `childScope`,
/// the resulting scopes will be:
/// ```ts
/// parentScope1: {
/// newScope: {
/// childScope: { }
/// }
/// childScope2: { }
/// }
/// ```
/// This is a shortcut for `ctx.scoping.insert_scope_between`.
pub fn insert_scope_between(
&mut self,
parent_id: ScopeId,
child_id: ScopeId,
flags: ScopeFlags,
) -> ScopeId {
self.scoping.insert_scope_between(parent_id, child_id, flags)
}
/// Remove scope for an expression from the scope chain.
///
/// Delete the scope and set parent of its child scopes to its parent scope.
/// e.g.:
/// * Starting scopes parentage `A -> B`, `B -> C`, `B -> D`.
/// * Remove scope `B` from chain.
/// * End result: scopes `A -> C`, `A -> D`.
///
/// Use this when removing an expression which owns a scope, without removing its children.
/// For example when unwrapping `(() => foo)()` to just `foo`.
/// `foo` here could be an expression which itself contains scopes.
///
/// This is a shortcut for `ctx.scoping.remove_scope_for_expression`.
pub fn remove_scope_for_expression(&mut self, scope_id: ScopeId, expr: &Expression) {
self.scoping.remove_scope_for_expression(scope_id, expr);
}
/// Generate binding.
///
/// Creates a symbol with the provided name and flags and adds it to the specified scope.
///
/// This is a shortcut for `ctx.scoping.generate_binding`.
pub fn generate_binding(
&mut self,
name: Ident<'a>,
scope_id: ScopeId,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.scoping.generate_binding(name, scope_id, flags)
}
/// Generate binding in current scope.
///
/// Creates a symbol with the provided name and flags and adds it to the current scope.
///
/// This is a shortcut for `ctx.scoping.generate_binding_in_current_scope`.
pub fn generate_binding_in_current_scope(
&mut self,
name: Ident<'a>,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.scoping.generate_binding_in_current_scope(name, flags)
}
/// Generate UID var name.
///
/// Finds a unique variable name which does clash with any other variables used in the program.
///
/// See [`TraverseScoping::generate_uid_name`] for important information on how UIDs are generated.
/// There are some potential "gotchas".
///
/// This is a shortcut for `ctx.scoping.generate_uid_name`.
pub fn generate_uid_name(&mut self, name: &str) -> Ident<'a> {
self.scoping.generate_uid_name(name, self.ast.allocator)
}
/// Generate UID.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid(
&mut self,
name: &str,
scope_id: ScopeId,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
// Get name for UID
let name = self.generate_uid_name(name);
let symbol_id = self.scoping.add_binding(name, scope_id, flags);
BoundIdentifier::new(name, symbol_id)
}
/// Generate UID in current scope.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid_in_current_scope(
&mut self,
name: &str,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.generate_uid(name, self.current_scope_id(), flags)
}
/// Generate UID in root scope.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid_in_root_scope(
&mut self,
name: &str,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.generate_uid(name, self.scoping().root_scope_id(), flags)
}
/// Generate UID based on node.
///
/// Recursively gathers the identifying names of a node, and joins them with `$`.
///
/// Based on Babel's `scope.generateUidBasedOnNode` logic.
/// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L543>
#[inline]
pub fn generate_uid_based_on_node<N: GatherNodeParts<'a>>(
&mut self,
node: &N,
scope_id: ScopeId,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
let name = get_var_name_from_node(node);
self.generate_uid(&name, scope_id, flags)
}
/// Generate UID in current scope based on node.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid_in_current_scope_based_on_node<N: GatherNodeParts<'a>>(
&mut self,
node: &N,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.generate_uid_based_on_node(node, self.current_scope_id(), flags)
}
/// Generate UID in current hoist scope.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid_in_current_hoist_scope(&mut self, name: &str) -> BoundIdentifier<'a> {
self.generate_uid(name, self.current_hoist_scope_id(), SymbolFlags::FunctionScopedVariable)
}
/// Generate UID in current hoist scope based on node.
///
/// See also comments on [`TraverseScoping::generate_uid_name`] for important information
/// on how UIDs are generated. There are some potential "gotchas".
#[inline]
pub fn generate_uid_in_current_hoist_scope_based_on_node<N: GatherNodeParts<'a>>(
&mut self,
node: &N,
) -> BoundIdentifier<'a> {
let name = get_var_name_from_node(node);
self.generate_uid_in_current_hoist_scope(&name)
}
/// Create a reference bound to a `SymbolId`.
///
/// This is a shortcut for `ctx.scoping.create_bound_reference`.
#[inline]
pub fn create_bound_reference(
&mut self,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> ReferenceId {
self.scoping.create_bound_reference(symbol_id, flags)
}
/// Create an `IdentifierReference` bound to a `SymbolId`.
pub fn create_bound_ident_reference(
&mut self,
span: Span,
name: Ident<'a>,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> IdentifierReference<'a> {
let reference_id = self.create_bound_reference(symbol_id, flags);
self.ast.identifier_reference_with_reference_id(span, name, reference_id)
}
/// Create an `Expression::Identifier` bound to a `SymbolId`.
pub fn create_bound_ident_expr(
&mut self,
span: Span,
name: Ident<'a>,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> Expression<'a> {
let ident = self.create_bound_ident_reference(span, name, symbol_id, flags);
Expression::Identifier(self.ast.alloc(ident))
}
/// Create an unbound reference.
///
/// This is a shortcut for `ctx.scoping.create_unbound_reference`.
#[inline]
pub fn create_unbound_reference(
&mut self,
name: Ident<'_>,
flags: ReferenceFlags,
) -> ReferenceId {
self.scoping.create_unbound_reference(name, flags)
}
/// Create an unbound `IdentifierReference`.
pub fn create_unbound_ident_reference(
&mut self,
span: Span,
name: Ident<'a>,
flags: ReferenceFlags,
) -> IdentifierReference<'a> {
let reference_id = self.create_unbound_reference(name, flags);
self.ast.identifier_reference_with_reference_id(span, name, reference_id)
}
/// Create an unbound `Expression::Identifier`.
pub fn create_unbound_ident_expr(
&mut self,
span: Span,
name: Ident<'a>,
flags: ReferenceFlags,
) -> Expression<'a> {
let ident = self.create_unbound_ident_reference(span, name, flags);
Expression::Identifier(self.ast.alloc(ident))
}
/// Create a reference optionally bound to a `SymbolId`.
///
/// If you know if there's a `SymbolId` or not, prefer `TraverseCtx::create_bound_reference`
/// or `TraverseCtx::create_unbound_reference`.
///
/// This is a shortcut for `ctx.scoping.create_reference`.
#[inline]
pub fn create_reference(
&mut self,
name: Ident<'_>,
symbol_id: Option<SymbolId>,
flags: ReferenceFlags,
) -> ReferenceId {
self.scoping.create_reference(name, symbol_id, flags)
}
/// Create an `IdentifierReference` optionally bound to a `SymbolId`.
///
/// If you know if there's a `SymbolId` or not, prefer `TraverseCtx::create_bound_ident_reference`
/// or `TraverseCtx::create_unbound_ident_reference`.
pub fn create_ident_reference(
&mut self,
span: Span,
name: Ident<'a>,
symbol_id: Option<SymbolId>,
flags: ReferenceFlags,
) -> IdentifierReference<'a> {
if let Some(symbol_id) = symbol_id {
self.create_bound_ident_reference(span, name, symbol_id, flags)
} else {
self.create_unbound_ident_reference(span, name, flags)
}
}
/// Create an `Expression::Identifier` optionally bound to a `SymbolId`.
///
/// If you know if there's a `SymbolId` or not, prefer `TraverseCtx::create_bound_ident_expr`
/// or `TraverseCtx::create_unbound_ident_expr`.
pub fn create_ident_expr(
&mut self,
span: Span,
name: Ident<'a>,
symbol_id: Option<SymbolId>,
flags: ReferenceFlags,
) -> Expression<'a> {
if let Some(symbol_id) = symbol_id {
self.create_bound_ident_expr(span, name, symbol_id, flags)
} else {
self.create_unbound_ident_expr(span, name, flags)
}
}
/// Create reference in current scope, looking up binding for `name`,
///
/// This is a shortcut for `ctx.scoping.create_reference_in_current_scope`.
#[inline]
pub fn create_reference_in_current_scope(
&mut self,
name: Ident<'_>,
flags: ReferenceFlags,
) -> ReferenceId {
self.scoping.create_reference_in_current_scope(name, flags)
}
/// Delete a reference.
///
/// Provided `name` must match `reference_id`.
///
/// This is a shortcut for `ctx.scoping.delete_reference`.
pub fn delete_reference(&mut self, reference_id: ReferenceId, name: Ident<'_>) {
self.scoping.delete_reference(reference_id, name);
}
/// Delete reference for an `IdentifierReference`.
///
/// This is a shortcut for `ctx.scoping.delete_reference_for_identifier`.
pub fn delete_reference_for_identifier(&mut self, ident: &IdentifierReference) {
self.scoping.delete_reference_for_identifier(ident);
}
}
// Methods used internally within crate
impl<'a, State> TraverseCtx<'a, State> {
/// Create new traversal context.
///
/// # SAFETY
/// This function must not be public to maintain soundness of [`TraverseAncestry`].
pub(crate) fn new(state: State, scoping: Scoping, allocator: &'a Allocator) -> Self {
let ancestry = TraverseAncestry::new();
let scoping = TraverseScoping::new(scoping);
let ast = AstBuilder::new(allocator);
Self { state, ancestry, scoping, ast }
}
/// Shortcut for `self.ancestry.push_stack`, to make `walk_*` methods less verbose.
///
/// # SAFETY
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
pub(crate) fn push_stack(&mut self, ancestor: Ancestor<'a, 'static>) -> PopToken {
self.ancestry.push_stack(ancestor)
}
/// Shortcut for `self.ancestry.pop_stack`, to make `walk_*` methods less verbose.
///
/// # SAFETY
/// See safety constraints of `TraverseAncestry.pop_stack`.
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
pub(crate) unsafe fn pop_stack(&mut self, token: PopToken) {
self.ancestry.pop_stack(token);
}
/// Shortcut for `self.ancestry.retag_stack`, to make `walk_*` methods less verbose.
///
/// # SAFETY
/// See safety constraints of `TraverseAncestry.retag_stack`.
/// This method must not be public outside this crate, or consumer could break safety invariants.
#[inline]
pub(crate) unsafe fn retag_stack(&mut self, ty: AncestorType) {
// SAFETY: Caller muct uphold safety constraints
unsafe { self.ancestry.retag_stack(ty) };
}
/// Shortcut for `ctx.scoping.set_current_scope_id`, to make `walk_*` methods less verbose.
#[inline]
pub(crate) fn set_current_scope_id(&mut self, scope_id: ScopeId) {
self.scoping.set_current_scope_id(scope_id);
}
/// Shortcut for `ctx.scoping.set_current_hoist_scope_id`, to make `walk_*` methods less verbose.
#[inline]
pub(crate) fn set_current_hoist_scope_id(&mut self, scope_id: ScopeId) {
self.scoping.set_current_hoist_scope_id(scope_id);
}
/// Shortcut for `ctx.scoping.set_current_block_scope_id`, to make `walk_*` methods less verbose.
#[inline]
pub(crate) fn set_current_block_scope_id(&mut self, scope_id: ScopeId) {
self.scoping.set_current_block_scope_id(scope_id);
}
}
@@ -0,0 +1,40 @@
use oxc_allocator::Allocator;
use oxc_semantic::Scoping;
use super::TraverseCtx;
/// Wrapper around [`TraverseCtx`], allowing its reuse.
///
/// We cannot expose ability to obtain an owned [`TraverseCtx`], as it's then possible to circumvent
/// the safety invariants of [`TraverseAncestry`].
///
/// This wrapper type can safely be passed to user code as only ways it can be used are to:
///
/// * Call `traverse_mut_with_ctx`, which maintains safety invariants.
/// * Unwrap it to [`Scoping`] and / or `State`, which discards the sensitive [`TraverseAncestry`] in the process.
///
/// [`TraverseAncestry`]: super::TraverseAncestry
#[repr(transparent)]
pub struct ReusableTraverseCtx<'a, State>(TraverseCtx<'a, State>);
// Public methods
impl<'a, State> ReusableTraverseCtx<'a, State> {
/// Create new [`ReusableTraverseCtx`].
pub fn new(state: State, scoping: Scoping, allocator: &'a Allocator) -> Self {
Self(TraverseCtx::new(state, scoping, allocator))
}
/// Get a reference to the user state.
pub fn state(&self) -> &State {
&self.0.state
}
}
// Internal methods
impl<'a, State> ReusableTraverseCtx<'a, State> {
/// Mutably borrow [`TraverseCtx`] from a [`ReusableTraverseCtx`].
#[inline] // because this function is a no-op at run time
pub(crate) fn get_mut(&mut self) -> &mut TraverseCtx<'a, State> {
&mut self.0
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,391 @@
use std::str;
use oxc_allocator::{Allocator, Vec as ArenaVec};
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
use oxc_semantic::{NodeId, Reference, Scoping};
use oxc_span::{Ident, SPAN};
use oxc_syntax::{
reference::{ReferenceFlags, ReferenceId},
scope::{ScopeFlags, ScopeId},
symbol::{SymbolFlags, SymbolId},
};
use super::{
bound_identifier::BoundIdentifier, scopes_collector::ChildScopeCollector, uid::UidGenerator,
};
/// Traverse scope context.
///
/// Contains the scope tree and symbols table, and provides methods to access them.
///
/// `current_scope_id` is the ID of current scope during traversal.
/// `walk_*` functions update this field when entering/exiting a scope.
pub struct TraverseScoping<'a> {
scoping: Scoping,
uid_generator: Option<UidGenerator<'a>>,
current_scope_id: ScopeId,
current_hoist_scope_id: ScopeId,
current_block_scope_id: ScopeId,
}
// Public methods
impl<'a> TraverseScoping<'a> {
/// Get current scope ID
#[inline]
pub fn current_scope_id(&self) -> ScopeId {
self.current_scope_id
}
/// Get current var hoisting scope ID
#[inline]
pub(crate) fn current_hoist_scope_id(&self) -> ScopeId {
self.current_hoist_scope_id
}
/// Get current block scope ID
#[inline]
pub(crate) fn current_block_scope_id(&self) -> ScopeId {
self.current_block_scope_id
}
/// Get current scope flags
#[inline]
pub fn current_scope_flags(&self) -> ScopeFlags {
self.scoping.scope_flags(self.current_scope_id)
}
/// Get scopes tree
#[inline]
pub fn scoping(&self) -> &Scoping {
&self.scoping
}
/// Get mutable scopes tree
#[inline]
pub fn scoping_mut(&mut self) -> &mut Scoping {
&mut self.scoping
}
/// Get iterator over scopes, starting with current scope and working up
pub fn ancestor_scopes(&self) -> impl Iterator<Item = ScopeId> + '_ {
self.scoping.scope_ancestors(self.current_scope_id)
}
/// Create new scope as child of provided scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn create_child_scope(&mut self, parent_id: ScopeId, flags: ScopeFlags) -> ScopeId {
let flags = self.scoping.get_new_scope_flags(flags, parent_id);
self.scoping.add_scope(Some(parent_id), NodeId::DUMMY, flags)
}
/// Create new scope as child of current scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn create_child_scope_of_current(&mut self, flags: ScopeFlags) -> ScopeId {
self.create_child_scope(self.current_scope_id, flags)
}
/// Insert a scope into scope tree below a statement.
///
/// Statement must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_statement(&mut self, stmt: &Statement, flags: ScopeFlags) -> ScopeId {
self.insert_scope_below_statement_from_scope_id(stmt, self.current_scope_id, flags)
}
/// Insert a scope into scope tree below a statement.
///
/// Statement must be in provided scope.
/// New scope is created as child of the provided scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_statement_from_scope_id(
&mut self,
stmt: &Statement,
scope_id: ScopeId,
flags: ScopeFlags,
) -> ScopeId {
let mut collector = ChildScopeCollector::new();
collector.visit_statement(stmt);
self.insert_scope_below(scope_id, &collector.scope_ids, flags)
}
/// Insert a scope into scope tree below an expression.
///
/// Expression must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the expression are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_expression(
&mut self,
expr: &Expression,
flags: ScopeFlags,
) -> ScopeId {
let mut collector = ChildScopeCollector::new();
collector.visit_expression(expr);
self.insert_scope_below(self.current_scope_id, &collector.scope_ids, flags)
}
/// Insert a scope into scope tree below a `Vec` of statements.
///
/// Statements must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_statements(
&mut self,
stmts: &ArenaVec<Statement>,
flags: ScopeFlags,
) -> ScopeId {
let mut collector = ChildScopeCollector::new();
collector.visit_statements(stmts);
self.insert_scope_below(self.current_scope_id, &collector.scope_ids, flags)
}
fn insert_scope_below(
&mut self,
scope_id: ScopeId,
child_scope_ids: &[ScopeId],
flags: ScopeFlags,
) -> ScopeId {
// Create new scope as child of parent
let new_scope_id = self.create_child_scope(scope_id, flags);
// Set scopes as children of new scope instead
for &child_id in child_scope_ids {
self.scoping.set_scope_parent_id(child_id, Some(new_scope_id));
}
new_scope_id
}
/// Insert a scope between a parent and a child scope.
///
/// For example, given the following scopes
/// ```ts
/// parentScope1: {
/// childScope: { }
/// childScope2: { }
/// }
/// ```
/// and calling this function with `parentScope1` and `childScope`,
/// the resulting scopes will be:
/// ```ts
/// parentScope1: {
/// newScope: {
/// childScope: { }
/// }
/// childScope2: { }
/// }
/// ```
pub fn insert_scope_between(
&mut self,
parent_id: ScopeId,
child_id: ScopeId,
flags: ScopeFlags,
) -> ScopeId {
let scope_id = self.create_child_scope(parent_id, flags);
debug_assert_eq!(
self.scoping.scope_parent_id(child_id),
Some(parent_id),
"Child scope must be a child of parent scope"
);
self.scoping.set_scope_parent_id(child_id, Some(scope_id));
scope_id
}
/// Remove scope for an expression from the scope chain.
///
/// Delete the scope and set parent of its child scopes to its parent scope.
/// e.g.:
/// * Starting scopes parentage `A -> B`, `B -> C`, `B -> D`.
/// * Remove scope `B` from chain.
/// * End result: scopes `A -> C`, `A -> D`.
///
/// Use this when removing an expression which owns a scope, without removing its children.
/// For example when unwrapping `(() => foo)()` to just `foo`.
/// `foo` here could be an expression which itself contains scopes.
pub fn remove_scope_for_expression(&mut self, scope_id: ScopeId, expr: &Expression) {
let mut collector = ChildScopeCollector::new();
collector.visit_expression(expr);
let child_ids = collector.scope_ids;
if !child_ids.is_empty() {
let parent_id = self.scoping.scope_parent_id(scope_id);
for child_id in child_ids {
self.scoping.set_scope_parent_id(child_id, parent_id);
}
}
}
/// Add binding to `ScopeTree` and `SymbolTable`.
#[inline]
pub(crate) fn add_binding(
&mut self,
name: Ident<'_>,
scope_id: ScopeId,
flags: SymbolFlags,
) -> SymbolId {
let symbol_id = self.scoping.create_symbol(SPAN, name, flags, scope_id, NodeId::DUMMY);
self.scoping.add_binding(scope_id, name, symbol_id);
symbol_id
}
/// Generate binding.
///
/// Creates a symbol with the provided name and flags and adds it to the specified scope.
pub fn generate_binding(
&mut self,
name: Ident<'a>,
scope_id: ScopeId,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
let symbol_id = self.add_binding(name, scope_id, flags);
BoundIdentifier::new(name, symbol_id)
}
/// Generate binding in current scope.
///
/// Creates a symbol with the provided name and flags and adds it to the current scope.
pub fn generate_binding_in_current_scope(
&mut self,
name: Ident<'a>,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.generate_binding(name, self.current_scope_id, flags)
}
/// Generate UID var name.
///
/// Finds a unique variable name which does clash with any other variables used in the program.
///
/// Caller must ensure `name` is a valid JS identifier, after a `_` is prepended on start.
/// The fact that a `_` will be prepended on start means providing an empty string or a string
/// starting with a digit (0-9) is fine.
///
/// See comments on `UidGenerator` for further details.
pub fn generate_uid_name(&mut self, name: &str, allocator: &'a Allocator) -> Ident<'a> {
// If `uid_generator` is not already populated, initialize it
let uid_generator =
self.uid_generator.get_or_insert_with(|| UidGenerator::new(&self.scoping, allocator));
// Generate unique name
uid_generator.create(name)
}
/// Create a reference bound to a `SymbolId`
pub fn create_bound_reference(
&mut self,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> ReferenceId {
let reference =
Reference::new_with_symbol_id(NodeId::DUMMY, symbol_id, self.current_scope_id, flags);
let reference_id = self.scoping.create_reference(reference);
self.scoping.add_resolved_reference(symbol_id, reference_id);
reference_id
}
/// Create an unbound reference
pub fn create_unbound_reference(
&mut self,
name: Ident<'_>,
flags: ReferenceFlags,
) -> ReferenceId {
let reference = Reference::new(NodeId::DUMMY, self.current_scope_id, flags);
let reference_id = self.scoping.create_reference(reference);
self.scoping.add_root_unresolved_reference(name, reference_id);
reference_id
}
/// Create a reference optionally bound to a `SymbolId`.
///
/// If you know if there's a `SymbolId` or not, prefer `TraverseCtx::create_bound_reference`
/// or `TraverseCtx::create_unbound_reference`.
pub fn create_reference(
&mut self,
name: Ident<'_>,
symbol_id: Option<SymbolId>,
flags: ReferenceFlags,
) -> ReferenceId {
if let Some(symbol_id) = symbol_id {
self.create_bound_reference(symbol_id, flags)
} else {
self.create_unbound_reference(name, flags)
}
}
/// Create reference in current scope, looking up binding for `name`
pub fn create_reference_in_current_scope(
&mut self,
name: Ident<'_>,
flags: ReferenceFlags,
) -> ReferenceId {
let symbol_id = self.scoping.find_binding(self.current_scope_id, name);
self.create_reference(name, symbol_id, flags)
}
/// Delete a reference.
///
/// Provided `name` must match `reference_id`.
pub fn delete_reference(&mut self, reference_id: ReferenceId, name: Ident<'_>) {
let symbol_id = self.scoping.get_reference(reference_id).symbol_id();
if let Some(symbol_id) = symbol_id {
self.scoping.delete_resolved_reference(symbol_id, reference_id);
} else {
self.scoping.delete_root_unresolved_reference(name, reference_id);
}
}
/// Delete reference for an `IdentifierReference`.
pub fn delete_reference_for_identifier(&mut self, ident: &IdentifierReference) {
self.delete_reference(ident.reference_id(), ident.name);
}
}
// Methods used internally within crate
impl TraverseScoping<'_> {
/// Create new `TraverseScoping`
pub(super) fn new(scoping: Scoping) -> Self {
Self {
scoping,
uid_generator: None,
// Dummy values. Both immediately overwritten in `walk_program`.
current_scope_id: ScopeId::new(0),
current_hoist_scope_id: ScopeId::new(0),
current_block_scope_id: ScopeId::new(0),
}
}
/// Set current scope ID
#[inline]
pub(crate) fn set_current_scope_id(&mut self, scope_id: ScopeId) {
self.current_scope_id = scope_id;
}
/// Set current hoist scope ID
#[inline]
pub(crate) fn set_current_hoist_scope_id(&mut self, scope_id: ScopeId) {
self.current_hoist_scope_id = scope_id;
}
/// Set current block scope ID
#[inline]
pub(crate) fn set_current_block_scope_id(&mut self, scope_id: ScopeId) {
self.current_block_scope_id = scope_id;
}
pub fn delete_typescript_bindings(&mut self) {
self.scoping.delete_typescript_bindings();
}
}
@@ -0,0 +1,374 @@
use std::str;
use itoa::Buffer as ItoaBuffer;
use rustc_hash::FxHashMap;
use oxc_allocator::{Allocator, StringBuilder as ArenaStringBuilder};
use oxc_semantic::Scoping;
use oxc_str::Ident;
/// Unique identifier generator.
///
/// When initialized with [`UidGenerator::new`], creates a catalog of all symbols and unresolved references
/// in the AST which begin with `_`.
///
/// [`UidGenerator::create`] uses that catalog to generate a unique identifier which does not clash with
/// any existing name.
///
/// Such UIDs are based on the base name provided. They start with `_` and end with digits if required to
/// maintain uniqueness. e.g. given base name of `foo`, UIDs will be `_foo`, `_foo2`, `_foo3` etc.
///
/// Roughly based on Babel's `scope.generateUid` logic, but with some differences (see below).
/// <https://github.com/babel/babel/blob/3b1a3c0be9df65140260a316c1a21adcf948645d/packages/babel-traverse/src/scope/index.ts#L501-L523>
///
/// # Algorithm
///
/// UIDs are generated in series for each "base" name.
/// "Base" name is the provided name with `_`s trimmed from the start, and digits trimmed from the end.
///
/// During cataloging of existing symbols, for each base name it's recorded:
///
/// 1. Largest number of leading `_`s.
/// 2. Largest numeric postfix for that base name.
///
/// UIDs are generated for that base name with that number of leading underscores, and with ascending
/// numeric postfix.
///
/// | Existing symbols | Generated UIDs |
/// |------------------|---------------------------------|
/// | (none) | `_foo`, `_foo2`, `_foo3` |
/// | `_foo` | `_foo2`, `_foo3`, `_foo4` |
/// | `_foo3` | `_foo4`, `_foo5`, `_foo6` |
/// | `__foo` | `__foo2`, `__foo3`, `__foo4` |
/// | `___foo5` | `___foo6`, `___foo7`, `___foo8` |
/// | `_foo8`, `__foo` | `__foo2`, `__foo3`, `__foo4` |
///
/// This algorithm requires at most 1 hashmap lookup and 1 hashmap insert per UID generated.
///
/// # Differences from Babel
///
/// This implementation aims to replicate Babel's behavior, but differs from Babel
/// in the following ways:
///
/// 1. Does not check that name provided as "base" for the UID is a valid JS identifier name.
/// In most cases, we're creating a UID based on an existing variable name, in which case
/// this check is redundant.
/// Caller must ensure `name` is a valid JS identifier, after a `_` is prepended on start.
/// The fact that a `_` will be prepended on start means providing an empty string or a string
/// starting with a digit (0-9) is fine.
///
/// 2. Does not convert to camel case.
/// This seems unimportant.
///
/// 3. Does not check var name against list of globals or "contextVariables"
/// (which Babel does in `hasBinding`).
/// No globals or "contextVariables" start with `_` anyway, so no need for this check.
///
/// 4. Does not check this name is unique if used as a named statement label,
/// only that it's unique as an identifier.
///
/// 5. Uses a slightly different algorithm for generating names (see above).
/// The resulting UIDs are similar enough to Babel's algorithm to fail only 1 of Babel's tests.
///
/// # Potential improvements
///
/// TODO(improve-on-babel):
///
/// UID generation is fairly expensive, because of the amount of string hashing required.
///
/// [`UidGenerator::new`] iterates through every binding and unresolved reference in the entire AST,
/// and builds a hashmap of symbols which could clash with UIDs.
/// Once that's built, [`UidGenerator::create`] has to do at a hashmap lookup when generating each UID.
/// Hashing strings is a fairly expensive operation.
///
/// We could improve this in one of 3 ways:
///
/// ## 1. Build the hashmap in `SemanticBuilder`
///
/// Instead of iterating through all symbols again here.
///
/// ## 2. Use a simpler algorithm
///
/// * During initial semantic pass, check for any existing identifiers starting with `_`.
/// * Calculate what is the highest postfix number on `_...` identifiers (e.g. `_foo1`, `_bar8`).
/// * Store that highest number in a counter which is global across the whole program.
/// * When creating a UID, increment the counter, and make the UID `_<name><counter>`.
///
/// i.e. if source contains identifiers `_foo1` and `_bar15`, create UIDs named `_qux16`,
/// `_temp17` etc. They'll all be unique within the program.
///
/// Minimal cost in semantic, and generating UIDs extremely cheap.
///
/// The resulting UIDs would still be fairly readable.
///
/// This is a different method from Babel, and unfortunately produces UID names
/// which differ from Babel for some of its test cases.
///
/// ## 3. Even simpler algorithm, but produces hard-to-read code
///
/// If output is being minified anyway, use a method which produces less debuggable output,
/// but is even simpler:
///
/// * During initial semantic pass, check for any existing identifiers starting with `_`.
/// * Find the highest number of leading `_`s for any existing symbol.
/// * Generate UIDs with a counter starting at 0, prefixed with number of `_`s one greater than
/// what was found in AST.
///
/// i.e. if source contains identifiers `_foo` and `__bar`, create UIDs names `___0`, `___1`,
/// `___2` etc. They'll all be unique within the program.
pub struct UidGenerator<'a> {
names: FxHashMap<&'a str, UidName>,
allocator: &'a Allocator,
}
/// Details of next UID for a base name.
//
// `#[repr(align(8))]` on 64-bit platforms so can fit in a single register.
#[cfg_attr(target_pointer_width = "64", repr(align(8)))]
#[derive(Clone, Copy)]
struct UidName {
/// Digits appended to end of name.
/// When generating a UID, increment this field and use that as the postfix.
/// This field is never 0, so postfix will be at least 2.
postfix: u32,
/// Number of underscores to prefix name with.
underscore_count: u32,
}
impl<'a> UidGenerator<'a> {
/// Create [`UidGenerator`].
pub(super) fn new(scoping: &Scoping, allocator: &'a Allocator) -> Self {
let mut generator = Self { names: FxHashMap::default(), allocator };
for name in scoping.symbol_names() {
generator.add(name);
}
for name in scoping.root_unresolved_references().keys() {
generator.add(name.as_str());
}
generator
}
/// Add a record to [`UidGenerator`].
fn add(&mut self, name: &str) {
// If `name` does not start with `_`, exit
if name.as_bytes().first() != Some(&b'_') {
return;
}
// Trim off underscores from start of `name`
let original_len = name.len();
// SAFETY: We just check first byte of `name` is `_`
let name = unsafe { name.get_unchecked(1..) };
let mut name = name.trim_start_matches('_');
#[expect(clippy::cast_possible_truncation)]
let underscore_count = (original_len - name.len()) as u32;
let mut uid_name = UidName { underscore_count, postfix: 1 };
// Find digits on end of `name`
let last_non_digit_index = name.as_bytes().iter().rposition(|&b| !b.is_ascii_digit());
let parts = match last_non_digit_index {
Some(last_non_digit_index) => {
if last_non_digit_index == name.len() - 1 {
// No digits on end
None
} else {
// Name ends with digits
let digit_index = last_non_digit_index + 1;
debug_assert!(name.as_bytes().get(digit_index).is_some_and(u8::is_ascii_digit));
// SAFETY: There's an ASCII digit at `digit_index`, so slicing `name` at that index
// is guaranteed to yield 2 valid UTF-8 strings. `digit_index` cannot be out of bounds.
unsafe {
let without_digits = name.get_unchecked(..digit_index);
let digits = name.get_unchecked(digit_index..);
Some((without_digits, digits))
}
}
}
None => {
if name.is_empty() {
// Name consists purely of `_`s e.g. `_` or `___`
None
} else {
// Name consists of `_`s followed by digits e.g. `_123`
Some(("", name))
}
}
};
if let Some((without_digits, digits)) = parts {
const U32_MAX_LEN: usize = "4294967295".len(); // 4294967295 = u32::MAX
// SAFETY: `digits` cannot be empty
let first_digit = unsafe { *digits.as_bytes().get_unchecked(0) };
if first_digit == b'0' || digits.len() > U32_MAX_LEN {
// We don't create UIDs with postfix starting with 0, or greater than `u32::MAX`,
// so can ignore this - can't clash
return;
}
if let Ok(n) = digits.parse::<u32>() {
if n == 1 {
// We don't create UIDs with postfix of 1, so can ignore this - can't clash
return;
}
name = without_digits;
uid_name.postfix = n;
} else {
// Digits represent a number greater than `u32::MAX`.
// We don't create UIDs with postfix over `u32::MAX` so can ignore this - can't clash.
return;
}
}
// Unfortunately can't use `Entry` API here because `name` doesn't have required lifetime `'a`,
// because it comes from `Semantic`'s arena, not the AST arena
if let Some(existing_uid_name) = self.names.get_mut(name) {
if uid_name.underscore_count > existing_uid_name.underscore_count
|| (uid_name.underscore_count == existing_uid_name.underscore_count
&& uid_name.postfix > existing_uid_name.postfix)
{
*existing_uid_name = uid_name;
}
} else {
let name = self.allocator.alloc_str(name);
self.names.insert(name, uid_name);
}
}
/// Create a unique identifier.
///
/// The UID returned will be added to the list of used identifiers, so this method will never
/// return the same UID twice.
///
/// Caller must ensure `name` is a valid JS identifier, after a `_` is prepended on start.
/// The fact that a `_` will be prepended on start means providing an empty string or a string
/// starting with a digit (0-9) is fine.
///
/// Please see docs for [`UidGenerator`] for further info.
pub(super) fn create(&mut self, name: &str) -> Ident<'a> {
// Get the base name, with `_`s trimmed from start, and digits trimmed from end.
// i.e. `__foo123` -> `foo`.
// Equivalent to `name.trim_start_matches('_').trim_end_matches(|c: char| c.is_ascii_digit())`
// but more efficient as operates on bytes not chars
let mut bytes = name.as_bytes();
while bytes.first() == Some(&b'_') {
bytes = &bytes[1..];
}
while matches!(bytes.last(), Some(b) if b.is_ascii_digit()) {
bytes = &bytes[0..bytes.len() - 1];
}
// SAFETY: We started with a valid UTF8 `&str` and have only trimmed off ASCII characters,
// so remainder must still be valid UTF8
let base = unsafe { str::from_utf8_unchecked(bytes) };
// Generate UID.
// Unfortunately can't use `Entry` API here as `name` doesn't have required lifetime `'a`.
if let Some(uid_name) = self.names.get_mut(base) {
// AST contains identifier(s) with this base already.
// Get next postfix.
if uid_name.postfix < u32::MAX {
// Increment `postfix`
uid_name.postfix += 1;
} else {
// Identifier `_<base>4294967295` was already used.
// Can't increment `postfix` as it would wrap around, so increment `underscore_count` instead.
// It shouldn't be possible for `underscore_count` to be `u32::MAX` too, because that
// would require an identifier comprising `u32::MAX` x underscores in source text.
// That's theoretically possible, but source text is limited to `u32::MAX` bytes,
// so it'd be the entirety of the source text. Therefore `postfix` would be 1.
uid_name.underscore_count += 1;
uid_name.postfix = 2;
}
// Format UID `_<base><postfix>`.
// If `underscore_count > 1`, add further underscores to the start.
let mut buffer = ItoaBuffer::new();
let digits = buffer.format(uid_name.postfix);
if uid_name.underscore_count == 1 {
Ident::from_strs_array_in(["_", base, digits], self.allocator)
} else {
let mut uid = ArenaStringBuilder::with_capacity_in(
uid_name.underscore_count as usize + base.len() + digits.len(),
self.allocator,
);
uid.push_ascii_byte_repeat(b'_', uid_name.underscore_count as usize);
uid.push_str(base);
uid.push_str(digits);
Ident::from(uid)
}
} else {
let uid = Ident::from_strs_array_in(["_", base], self.allocator);
// SAFETY: String starts with `_`, so trimming off that byte leaves a valid UTF-8 string
let base = unsafe { uid.as_str().get_unchecked(1..) };
self.names.insert(base, UidName { underscore_count: 1, postfix: 1 });
uid
}
}
}
#[cfg(test)]
#[test]
fn uids() {
// (&[ initial, ... ], &[ (name, expected_uid), ... ])
#[expect(clippy::type_complexity)]
let cases: &[(&[&str], &[(&str, &str)])] = &[
(&[], &[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")]),
(
&["foo", "foo0", "foo1", "foo2", "foo10", "_bar"],
&[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")],
),
(
&["_foo0", "_foo1", "__foo0", "____foo1", "_foo01", "_foo012345", "_foo000000"],
&[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")],
),
(&[], &[("_foo", "_foo"), ("__foo", "_foo2"), ("_____foo", "_foo3")]),
(&[], &[("_foo123", "_foo"), ("__foo456", "_foo2"), ("_____foo789", "_foo3")]),
(&["_foo"], &[("foo", "_foo2"), ("foo", "_foo3"), ("foo", "_foo4")]),
(&["_foo3"], &[("foo", "_foo4"), ("foo", "_foo5"), ("foo", "_foo6")]),
(&["__foo"], &[("foo", "__foo2"), ("foo", "__foo3"), ("foo", "__foo4")]),
(&["__foo8"], &[("foo", "__foo9"), ("foo", "__foo10"), ("foo", "__foo11")]),
(&["_foo999", "____foo"], &[("foo", "____foo2"), ("foo", "____foo3"), ("foo", "____foo4")]),
(
&["_foo4294967293"],
&[
("foo", "_foo4294967294"),
("foo", "_foo4294967295"),
("foo", "__foo2"),
("foo", "__foo3"),
],
),
(
&["___foo4294967293"],
&[
("foo", "___foo4294967294"),
("foo", "___foo4294967295"),
("foo", "____foo2"),
("foo", "____foo3"),
],
),
(&[], &[("_", "_"), ("_", "_2"), ("_", "_3")]),
(
&["_0", "_1", "__0", "____1", "_01", "_012345", "_000000"],
&[("_", "_"), ("_", "_2"), ("_", "_3")],
),
(&[], &[("___", "_"), ("_____", "_2"), ("_____", "_3")]),
(&["_"], &[("_", "_2"), ("_", "_3"), ("_", "_4")]),
(&["_4"], &[("_", "_5"), ("_", "_6"), ("_", "_7")]),
(&["___"], &[("_", "___2"), ("_", "___3"), ("_", "___4")]),
(&["___99"], &[("_", "___100"), ("_", "___101"), ("_", "___102")]),
(&["_"], &[("_123", "_2"), ("__456", "_3"), ("___789", "_4")]),
];
let allocator = Allocator::default();
for &(used_names, created) in cases {
let mut generator = UidGenerator { names: FxHashMap::default(), allocator: &allocator };
for &used_name in used_names {
generator.add(used_name);
}
for &(name, uid) in created {
assert_eq!(generator.create(name), uid);
}
}
}
@@ -0,0 +1,34 @@
//! Generator for minifier-local traverse runtime.
//!
//! Generates 2 files in `oxc_minifier` crate:
//! * `traverse.rs` - `MinifierTraverse` trait with `enter_*` / `exit_*` methods.
//! * `walk.rs` - Unsafe `walk_*` functions for AST traversal.
use super::traverse::{self, TraverseTraitConfig};
use crate::{
Codegen, Generator, MINIFIER_CRATE_PATH,
output::{Output, output_path},
schema::Schema,
};
use super::define_generator;
pub struct MinifierTraverseGenerator;
define_generator!(MinifierTraverseGenerator);
impl Generator for MinifierTraverseGenerator {
fn generate_many(&self, schema: &Schema, _codegen: &Codegen) -> Vec<Output> {
let config = TraverseTraitConfig::minifier();
vec![
Output::Rust {
path: output_path(MINIFIER_CRATE_PATH, "traverse.rs"),
tokens: traverse::generate_traverse_trait(schema, &config),
},
Output::Rust {
path: output_path(MINIFIER_CRATE_PATH, "walk.rs"),
tokens: traverse::generate_walk_minifier(schema),
},
]
}
}
+2
View File
@@ -11,6 +11,7 @@ mod ast_kind;
mod estree_visit;
mod formatter;
mod get_id;
mod minifier_traverse;
#[cfg(feature = "generate-js")]
mod oxlint_envs;
#[cfg(feature = "generate-js")]
@@ -31,6 +32,7 @@ pub use ast_kind::AstKindGenerator;
pub use estree_visit::ESTreeVisitGenerator;
pub use formatter::{FormatterAstNodesGenerator, FormatterFormatGenerator};
pub use get_id::GetIdGenerator;
pub use minifier_traverse::MinifierTraverseGenerator;
#[cfg(feature = "generate-js")]
pub use oxlint_envs::OxlintEnvsGenerator;
#[cfg(feature = "generate-js")]
+54 -11
View File
@@ -5,8 +5,8 @@
//! * `walk.rs` - Unsafe `walk_*` functions for AST traversal.
//! * `ancestor.rs` - Ancestor tracking types and offset constants.
mod ancestor;
mod walk;
pub(super) mod ancestor;
pub(super) mod walk;
use proc_macro2::TokenStream;
use quote::quote;
@@ -27,14 +27,15 @@ define_generator!(TraverseGenerator);
impl Generator for TraverseGenerator {
fn generate_many(&self, schema: &Schema, _codegen: &Codegen) -> Vec<Output> {
let config = TraverseTraitConfig::traverse();
vec![
Output::Rust {
path: output_path(TRAVERSE_CRATE_PATH, "traverse.rs"),
tokens: generate_traverse_trait(schema),
tokens: generate_traverse_trait(schema, &config),
},
Output::Rust {
path: output_path(TRAVERSE_CRATE_PATH, "walk.rs"),
tokens: walk::generate_walk(schema),
tokens: generate_walk_traverse(schema),
},
Output::Rust {
path: output_path(TRAVERSE_CRATE_PATH, "ancestor.rs"),
@@ -44,8 +45,50 @@ impl Generator for TraverseGenerator {
}
}
pub(super) fn generate_walk_traverse(schema: &Schema) -> TokenStream {
walk::generate_walk(schema, &walk::WalkConfig::traverse())
}
pub(super) fn generate_walk_minifier(schema: &Schema) -> TokenStream {
walk::generate_walk(schema, &walk::WalkConfig::minifier())
}
pub(super) struct TraverseTraitConfig {
pub trait_ident: syn::Ident,
pub trait_generics: TokenStream,
pub ctx_ty: TokenStream,
pub ctx_use: TokenStream,
}
impl TraverseTraitConfig {
pub fn traverse() -> Self {
Self {
trait_ident: quote::format_ident!("Traverse"),
trait_generics: quote! { <'a, State> },
ctx_ty: quote! { TraverseCtx<'a, State> },
ctx_use: quote! { use crate::TraverseCtx; },
}
}
pub fn minifier() -> Self {
Self {
trait_ident: quote::format_ident!("Traverse"),
trait_generics: quote! { <'a> },
ctx_ty: quote! { TraverseCtx<'a> },
ctx_use: quote! { use crate::TraverseCtx; },
}
}
}
/// Generate `Traverse` trait with `enter_*` / `exit_*` methods for each visited type.
fn generate_traverse_trait(schema: &Schema) -> TokenStream {
pub(super) fn generate_traverse_trait(
schema: &Schema,
config: &TraverseTraitConfig,
) -> TokenStream {
let ctx_ty = &config.ctx_ty;
let trait_ident = &config.trait_ident;
let trait_generics = &config.trait_generics;
let ctx_use = &config.ctx_use;
let mut methods = quote!();
for type_def in &schema.types {
@@ -66,9 +109,9 @@ fn generate_traverse_trait(schema: &Schema) -> TokenStream {
methods.extend(quote! {
///@@line_break
#[inline]
fn #enter_ident(&mut self, node: &mut #ty, ctx: &mut TraverseCtx<'a, State>) {}
fn #enter_ident(&mut self, node: &mut #ty, ctx: &mut #ctx_ty) {}
#[inline]
fn #exit_ident(&mut self, node: &mut #ty, ctx: &mut TraverseCtx<'a, State>) {}
fn #exit_ident(&mut self, node: &mut #ty, ctx: &mut #ctx_ty) {}
});
}
@@ -76,9 +119,9 @@ fn generate_traverse_trait(schema: &Schema) -> TokenStream {
methods.extend(quote! {
///@@line_break
#[inline]
fn enter_statements(&mut self, node: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a, State>) {}
fn enter_statements(&mut self, node: &mut Vec<'a, Statement<'a>>, ctx: &mut #ctx_ty) {}
#[inline]
fn exit_statements(&mut self, node: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a, State>) {}
fn exit_statements(&mut self, node: &mut Vec<'a, Statement<'a>>, ctx: &mut #ctx_ty) {}
});
quote! {
@@ -86,11 +129,11 @@ fn generate_traverse_trait(schema: &Schema) -> TokenStream {
use oxc_ast::ast::*;
///@@line_break
use crate::TraverseCtx;
#ctx_use
///@@line_break
#[expect(unused_variables)]
pub trait Traverse<'a, State> {
pub trait #trait_ident #trait_generics {
#methods
}
}
+67 -14
View File
@@ -20,9 +20,45 @@ use super::{
traverse_snake_name,
};
pub(super) struct WalkConfig {
pub use_code: TokenStream,
pub trait_bound: TokenStream,
pub ctx_ty: TokenStream,
pub has_state: bool,
}
impl WalkConfig {
pub fn traverse() -> Self {
Self {
use_code: quote! { use crate::{ancestor::{self, AncestorType}, Ancestor, Traverse, TraverseCtx}; },
trait_bound: quote! { Tr: Traverse<'a, State> },
ctx_ty: quote! { TraverseCtx<'a, State> },
has_state: true,
}
}
pub fn minifier() -> Self {
Self {
use_code: quote! {
use crate::{
generated::ancestor::{self, Ancestor, AncestorType},
Traverse,
TraverseCtx,
};
},
trait_bound: quote! { Tr: Traverse<'a> },
ctx_ty: quote! { TraverseCtx<'a> },
has_state: false,
}
}
}
/// Generate all walk functions.
pub fn generate_walk(schema: &Schema) -> TokenStream {
pub(super) fn generate_walk(schema: &Schema, config: &WalkConfig) -> TokenStream {
let mut walk_methods = quote!();
let use_code = &config.use_code;
let ctx_ty = &config.ctx_ty;
let walk_generics = walk_generics_tokens(config);
for type_def in &schema.types {
if !is_ast_type_with_visitor(type_def, schema) {
@@ -30,10 +66,10 @@ pub fn generate_walk(schema: &Schema) -> TokenStream {
}
match type_def {
TypeDef::Struct(struct_def) => {
walk_methods.extend(generate_walk_for_struct(struct_def, schema));
walk_methods.extend(generate_walk_for_struct(struct_def, schema, config));
}
TypeDef::Enum(enum_def) => {
walk_methods.extend(generate_walk_for_enum(enum_def, schema));
walk_methods.extend(generate_walk_for_enum(enum_def, schema, config));
}
_ => {}
}
@@ -59,7 +95,7 @@ pub fn generate_walk(schema: &Schema) -> TokenStream {
use oxc_syntax::scope::ScopeId;
///@@line_break
use crate::{ancestor::{self, AncestorType}, Ancestor, Traverse, TraverseCtx};
#use_code
///@@line_break
/// Walk AST with `Traverse` impl.
@@ -69,10 +105,10 @@ pub fn generate_walk(schema: &Schema) -> TokenStream {
/// (`Program<'a>`).
/// * `ctx` must contain a `TraverseAncestry<'a>` with single `Ancestor::None` on its stack.
#[inline]
pub unsafe fn walk_ast<'a, State, Tr: Traverse<'a, State>>(
pub unsafe fn walk_ast<#walk_generics>(
traverser: &mut Tr,
program: *mut Program<'a>,
ctx: &mut TraverseCtx<'a, State>,
ctx: &mut #ctx_ty,
) {
walk_program(traverser, program, ctx);
}
@@ -80,10 +116,10 @@ pub fn generate_walk(schema: &Schema) -> TokenStream {
#walk_methods
///@@line_break
unsafe fn walk_statements<'a, State, Tr: Traverse<'a, State>>(
unsafe fn walk_statements<#walk_generics>(
traverser: &mut Tr,
stmts: *mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a, State>,
ctx: &mut #ctx_ty,
) {
traverser.enter_statements(&mut *stmts, ctx);
for stmt in &mut *stmts {
@@ -94,8 +130,23 @@ pub fn generate_walk(schema: &Schema) -> TokenStream {
}
}
fn walk_generics_tokens(config: &WalkConfig) -> TokenStream {
let trait_bound = &config.trait_bound;
if config.has_state {
quote! { 'a, State, #trait_bound }
} else {
quote! { 'a, #trait_bound }
}
}
/// Generate walk function for a struct type.
fn generate_walk_for_struct(struct_def: &StructDef, schema: &Schema) -> TokenStream {
fn generate_walk_for_struct(
struct_def: &StructDef,
schema: &Schema,
config: &WalkConfig,
) -> TokenStream {
let walk_generics = walk_generics_tokens(config);
let ctx_ty = &config.ctx_ty;
let visitor_names = struct_def.visit.visitor_names.as_ref().unwrap();
let snake_name = traverse_snake_name(visitor_names);
let walk_fn_name = format_ident!("walk_{snake_name}");
@@ -195,10 +246,10 @@ fn generate_walk_for_struct(struct_def: &StructDef, schema: &Schema) -> TokenStr
quote! {
///@@line_break
unsafe fn #walk_fn_name<'a, State, Tr: Traverse<'a, State>>(
unsafe fn #walk_fn_name<#walk_generics>(
traverser: &mut Tr,
node: *mut #struct_ty,
ctx: &mut TraverseCtx<'a, State>,
ctx: &mut #ctx_ty,
) {
traverser.#enter_fn_name(&mut *node, ctx);
#fields_code
@@ -472,7 +523,9 @@ fn generate_field_walk_option_with_retag(
}
/// Generate walk function for an enum type.
fn generate_walk_for_enum(enum_def: &EnumDef, schema: &Schema) -> TokenStream {
fn generate_walk_for_enum(enum_def: &EnumDef, schema: &Schema, config: &WalkConfig) -> TokenStream {
let walk_generics = walk_generics_tokens(config);
let ctx_ty = &config.ctx_ty;
let visitor_names = enum_def.visit.visitor_names.as_ref().unwrap();
let snake_name = traverse_snake_name(visitor_names);
let walk_fn_name = format_ident!("walk_{snake_name}");
@@ -528,10 +581,10 @@ fn generate_walk_for_enum(enum_def: &EnumDef, schema: &Schema) -> TokenStream {
quote! {
///@@line_break
unsafe fn #walk_fn_name<'a, State, Tr: Traverse<'a, State>>(
unsafe fn #walk_fn_name<#walk_generics>(
traverser: &mut Tr,
node: *mut #enum_ty,
ctx: &mut TraverseCtx<'a, State>,
ctx: &mut #ctx_ty,
) {
traverser.#enter_fn_name(&mut *node, ctx);
match &mut *node {
+4
View File
@@ -253,6 +253,9 @@ const AST_MACROS_CRATE_PATH: &str = "crates/oxc_ast_macros";
/// Path to `oxc_traverse` crate
const TRAVERSE_CRATE_PATH: &str = "crates/oxc_traverse";
/// Path to `oxc_minifier` crate
const MINIFIER_CRATE_PATH: &str = "crates/oxc_minifier";
/// Path to write TS type definitions to
const TYPESCRIPT_DEFINITIONS_PATH: &str = "npm/oxc-types/types.d.ts";
@@ -300,6 +303,7 @@ const GENERATORS: &[&(dyn Generator + Sync)] = &[
&generators::FormatterFormatGenerator,
&generators::FormatterAstNodesGenerator,
&generators::TraverseGenerator,
&generators::MinifierTraverseGenerator,
];
/// Attributes on structs and enums (not including those defined by derives/generators)