fix(traverse)!: remove unsound APIs (#7514)

It's essential to `oxc_traverse`'s safety scheme that the user cannot create a `TraverseAncestry`, because they could then substitute it for the one stored in `TraverseCtx`, and cause a buffer underrun when an ancestor gets popped off stack which should never be empty - but it is because user has sneakily swapped it for another one.

Not being able to create a `TraverseAncestry` also requires that user cannot obtain an owned `TraverseCtx` either, because you can obtain an owned `TraverseAncestry` from an owned `TraverseCtx`.

Therefore, it's unsound for `TraverseCtx::new` to be public.

However, it is useful in minifier to be able to re-use the same `TraverseCtx` over and over, which requires having an owned `TraverseCtx`.

To support this use case, introduce `ReusableTraverseCtx`. It is an opaque wrapper around `TraverseCtx`, which prevents accessing the `TraverseCtx` inside it. It's safe for user to own a `ReusableTraverseCtx`, because there's nothing they can do with it except for using it to traverse via `traverse_mut_with_ctx`, which ensures the safety invariants are upheld.

At some point, we'll hopefully be able to reduce the number of passes in the minifier, and so remove the need for `ReusableTraverseCtx`.But in the meantime, this keeps `Traverse`'s API safe from unsound abuse.

Note: Strictly speaking, there is still room to abuse the API and produce UB by initiating a 2nd traversal of a different AST in an `Traverse` visitor, and then `mem::swap` the 2 x `&mut TraverseCtx`s. But this is 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 real risk in practice. We'd need branded lifetimes to close that hole too.

So this PR doesn't 100% ensure safety in a formal sense, but it at least makes it very hard to trigger UB *by accident*, which was the risk before.
This commit is contained in:
overlookmotel 2024-11-28 01:20:33 +00:00 committed by Boshen
parent 99ecc687ba
commit f2f31a8d7a
No known key found for this signature in database
GPG key ID: 67715A371E534061
18 changed files with 181 additions and 106 deletions

View file

@ -1,6 +1,6 @@
use oxc_allocator::Vec; use oxc_allocator::Vec;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::CompressorPass; use crate::CompressorPass;
@ -17,9 +17,9 @@ impl<'a> CompressorPass<'a> for CollapseVariableDeclarations {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -1,5 +1,5 @@
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse};
use crate::CompressorPass; use crate::CompressorPass;
@ -15,9 +15,9 @@ impl<'a> CompressorPass<'a> for ExploitAssigns {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -20,12 +20,12 @@ pub use statement_fusion::StatementFusion;
use oxc_allocator::Vec; use oxc_allocator::Vec;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
pub trait CompressorPass<'a>: Traverse<'a> { pub trait CompressorPass<'a>: Traverse<'a> {
fn changed(&self) -> bool; fn changed(&self) -> bool;
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>); fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>);
} }
// See `latePeepholeOptimizations` // See `latePeepholeOptimizations`
@ -61,9 +61,9 @@ impl<'a> CompressorPass<'a> for EarlyPass {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
self.changed = self.x0_statement_fusion.changed() self.changed = self.x0_statement_fusion.changed()
|| self.x1_peephole_remove_dead_code.changed() || self.x1_peephole_remove_dead_code.changed()
|| self.x2_peephole_minimize_conditions.changed() || self.x2_peephole_minimize_conditions.changed()
@ -167,9 +167,9 @@ impl<'a> CompressorPass<'a> for LatePass {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
self.changed = self.x0_exploit_assigns.changed() self.changed = self.x0_exploit_assigns.changed()
|| self.x0_exploit_assigns.changed() || self.x0_exploit_assigns.changed()
|| self.x1_collapse_variable_declarations.changed() || self.x1_collapse_variable_declarations.changed()

View file

@ -8,7 +8,7 @@ use oxc_syntax::{
number::{NumberBase, ToJsString}, number::{NumberBase, ToJsString},
operator::{BinaryOperator, LogicalOperator}, operator::{BinaryOperator, LogicalOperator},
}; };
use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::{node_util::Ctx, CompressorPass}; use crate::{node_util::Ctx, CompressorPass};
@ -24,9 +24,9 @@ impl<'a> CompressorPass<'a> for PeepholeFoldConstants {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -1,7 +1,7 @@
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_ecmascript::ToBoolean; use oxc_ecmascript::ToBoolean;
use oxc_span::SPAN; use oxc_span::SPAN;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::CompressorPass; use crate::CompressorPass;
@ -21,9 +21,9 @@ impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -2,7 +2,7 @@ use oxc_allocator::Vec;
use oxc_ast::{ast::*, Visit}; use oxc_ast::{ast::*, Visit};
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, IsLiteralValue}; use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, IsLiteralValue};
use oxc_span::SPAN; use oxc_span::SPAN;
use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::node_util::Ctx; use crate::node_util::Ctx;
use crate::{keep_var::KeepVar, CompressorPass}; use crate::{keep_var::KeepVar, CompressorPass};
@ -22,9 +22,9 @@ impl<'a> CompressorPass<'a> for PeepholeRemoveDeadCode {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -4,7 +4,7 @@ use oxc_ecmascript::{
constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf,
StringLastIndexOf, StringSubstring, StringLastIndexOf, StringSubstring,
}; };
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::{node_util::Ctx, CompressorPass}; use crate::{node_util::Ctx, CompressorPass};
@ -19,9 +19,9 @@ impl<'a> CompressorPass<'a> for PeepholeReplaceKnownMethods {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -7,7 +7,7 @@ use oxc_syntax::{
number::NumberBase, number::NumberBase,
operator::{BinaryOperator, UnaryOperator}, operator::{BinaryOperator, UnaryOperator},
}; };
use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::{node_util::Ctx, CompressorPass}; use crate::{node_util::Ctx, CompressorPass};
@ -32,9 +32,9 @@ impl<'a> CompressorPass<'a> for PeepholeSubstituteAlternateSyntax {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -1,7 +1,7 @@
use oxc_allocator::Vec; use oxc_allocator::Vec;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_span::GetSpan; use oxc_span::GetSpan;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::{CompressOptions, CompressorPass}; use crate::{CompressOptions, CompressorPass};
@ -19,8 +19,8 @@ impl<'a> CompressorPass<'a> for RemoveSyntax {
false false
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -2,7 +2,7 @@ use oxc_allocator::Vec;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_ecmascript::side_effects::MayHaveSideEffects; use oxc_ecmascript::side_effects::MayHaveSideEffects;
use oxc_span::SPAN; use oxc_span::SPAN;
use oxc_traverse::{Traverse, TraverseCtx}; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
use crate::CompressorPass; use crate::CompressorPass;
@ -20,9 +20,9 @@ impl<'a> CompressorPass<'a> for StatementFusion {
self.changed self.changed
} }
fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
self.changed = false; self.changed = false;
oxc_traverse::walk_program(self, program, ctx); traverse_mut_with_ctx(self, program, ctx);
} }
} }

View file

@ -1,7 +1,7 @@
use oxc_allocator::Allocator; use oxc_allocator::Allocator;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_semantic::{ScopeTree, SemanticBuilder, SymbolTable}; use oxc_semantic::{ScopeTree, SemanticBuilder, SymbolTable};
use oxc_traverse::TraverseCtx; use oxc_traverse::ReusableTraverseCtx;
use crate::{ use crate::{
ast_passes::{ ast_passes::{
@ -33,7 +33,7 @@ impl<'a> Compressor<'a> {
scopes: ScopeTree, scopes: ScopeTree,
program: &mut Program<'a>, program: &mut Program<'a>,
) { ) {
let mut ctx = TraverseCtx::new(scopes, symbols, self.allocator); let mut ctx = ReusableTraverseCtx::new(scopes, symbols, self.allocator);
RemoveSyntax::new(self.options).build(program, &mut ctx); RemoveSyntax::new(self.options).build(program, &mut ctx);
if self.options.dead_code_elimination { if self.options.dead_code_elimination {
@ -62,7 +62,7 @@ impl<'a> Compressor<'a> {
LatePass::new().build(program, &mut ctx); LatePass::new().build(program, &mut ctx);
} }
fn dead_code_elimination(program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { fn dead_code_elimination(program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
PeepholeFoldConstants::new().build(program, ctx); PeepholeFoldConstants::new().build(program, ctx);
PeepholeMinimizeConditions::new().build(program, ctx); PeepholeMinimizeConditions::new().build(program, ctx);
PeepholeRemoveDeadCode::new().build(program, ctx); PeepholeRemoveDeadCode::new().build(program, ctx);

View file

@ -3,7 +3,7 @@ use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_parser::Parser; use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder; use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType; use oxc_span::SourceType;
use oxc_traverse::TraverseCtx; use oxc_traverse::ReusableTraverseCtx;
use crate::{ast_passes::CompressorPass, ast_passes::RemoveSyntax, CompressOptions}; use crate::{ast_passes::CompressorPass, ast_passes::RemoveSyntax, CompressOptions};
@ -40,7 +40,7 @@ fn run<'a, P: CompressorPass<'a>>(
if let Some(pass) = pass { if let Some(pass) = pass {
let (symbols, scopes) = let (symbols, scopes) =
SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree(); SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree();
let mut ctx = TraverseCtx::new(scopes, symbols, allocator); let mut ctx = ReusableTraverseCtx::new(scopes, symbols, allocator);
RemoveSyntax::new(CompressOptions::all_false()).build(&mut program, &mut ctx); RemoveSyntax::new(CompressOptions::all_false()).build(&mut program, &mut ctx);
pass.build(&mut program, &mut ctx); pass.build(&mut program, &mut ctx);
} }

View file

@ -281,7 +281,7 @@ impl<'a> Keys<'a> {
mod test { mod test {
use oxc_allocator::Allocator; use oxc_allocator::Allocator;
use oxc_semantic::{ScopeTree, SymbolTable}; use oxc_semantic::{ScopeTree, SymbolTable};
use oxc_traverse::TraverseCtx; use oxc_traverse::ReusableTraverseCtx;
use super::Keys; use super::Keys;
@ -290,7 +290,10 @@ mod test {
let allocator = Allocator::default(); let allocator = Allocator::default();
let scopes = ScopeTree::default(); let scopes = ScopeTree::default();
let symbols = SymbolTable::default(); let symbols = SymbolTable::default();
let mut $ctx = TraverseCtx::new(scopes, symbols, &allocator); let ctx = ReusableTraverseCtx::new(scopes, symbols, &allocator);
// SAFETY: Macro user only gets a `&mut TraverseCtx`, which cannot be abused
let mut ctx = unsafe { ctx.unwrap() };
let $ctx = &mut ctx;
}; };
} }
@ -300,18 +303,18 @@ mod test {
let mut keys = Keys::default(); let mut keys = Keys::default();
assert_eq!(keys.get_unique(&mut ctx), "_"); assert_eq!(keys.get_unique(ctx), "_");
assert_eq!(keys.get_unique(&mut ctx), "_2"); assert_eq!(keys.get_unique(ctx), "_2");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_4"); assert_eq!(keys.get_unique(ctx), "_4");
assert_eq!(keys.get_unique(&mut ctx), "_5"); assert_eq!(keys.get_unique(ctx), "_5");
assert_eq!(keys.get_unique(&mut ctx), "_6"); assert_eq!(keys.get_unique(ctx), "_6");
assert_eq!(keys.get_unique(&mut ctx), "_7"); assert_eq!(keys.get_unique(ctx), "_7");
assert_eq!(keys.get_unique(&mut ctx), "_8"); assert_eq!(keys.get_unique(ctx), "_8");
assert_eq!(keys.get_unique(&mut ctx), "_9"); assert_eq!(keys.get_unique(ctx), "_9");
assert_eq!(keys.get_unique(&mut ctx), "_10"); assert_eq!(keys.get_unique(ctx), "_10");
assert_eq!(keys.get_unique(&mut ctx), "_11"); assert_eq!(keys.get_unique(ctx), "_11");
assert_eq!(keys.get_unique(&mut ctx), "_12"); assert_eq!(keys.get_unique(ctx), "_12");
} }
#[test] #[test]
@ -328,9 +331,9 @@ mod test {
keys.reserve("_foo"); keys.reserve("_foo");
keys.reserve("_2foo"); keys.reserve("_2foo");
assert_eq!(keys.get_unique(&mut ctx), "_"); assert_eq!(keys.get_unique(ctx), "_");
assert_eq!(keys.get_unique(&mut ctx), "_2"); assert_eq!(keys.get_unique(ctx), "_2");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
} }
#[test] #[test]
@ -340,9 +343,9 @@ mod test {
let mut keys = Keys::default(); let mut keys = Keys::default();
keys.reserve("_"); keys.reserve("_");
assert_eq!(keys.get_unique(&mut ctx), "_2"); assert_eq!(keys.get_unique(ctx), "_2");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_4"); assert_eq!(keys.get_unique(ctx), "_4");
} }
#[test] #[test]
@ -354,15 +357,15 @@ mod test {
keys.reserve("_4"); keys.reserve("_4");
keys.reserve("_11"); keys.reserve("_11");
assert_eq!(keys.get_unique(&mut ctx), "_"); assert_eq!(keys.get_unique(ctx), "_");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_5"); assert_eq!(keys.get_unique(ctx), "_5");
assert_eq!(keys.get_unique(&mut ctx), "_6"); assert_eq!(keys.get_unique(ctx), "_6");
assert_eq!(keys.get_unique(&mut ctx), "_7"); assert_eq!(keys.get_unique(ctx), "_7");
assert_eq!(keys.get_unique(&mut ctx), "_8"); assert_eq!(keys.get_unique(ctx), "_8");
assert_eq!(keys.get_unique(&mut ctx), "_9"); assert_eq!(keys.get_unique(ctx), "_9");
assert_eq!(keys.get_unique(&mut ctx), "_10"); assert_eq!(keys.get_unique(ctx), "_10");
assert_eq!(keys.get_unique(&mut ctx), "_12"); assert_eq!(keys.get_unique(ctx), "_12");
} }
#[test] #[test]
@ -375,16 +378,16 @@ mod test {
keys.reserve("_12"); keys.reserve("_12");
keys.reserve("_13"); keys.reserve("_13");
assert_eq!(keys.get_unique(&mut ctx), "_"); assert_eq!(keys.get_unique(ctx), "_");
assert_eq!(keys.get_unique(&mut ctx), "_2"); assert_eq!(keys.get_unique(ctx), "_2");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_6"); assert_eq!(keys.get_unique(ctx), "_6");
assert_eq!(keys.get_unique(&mut ctx), "_7"); assert_eq!(keys.get_unique(ctx), "_7");
assert_eq!(keys.get_unique(&mut ctx), "_8"); assert_eq!(keys.get_unique(ctx), "_8");
assert_eq!(keys.get_unique(&mut ctx), "_9"); assert_eq!(keys.get_unique(ctx), "_9");
assert_eq!(keys.get_unique(&mut ctx), "_10"); assert_eq!(keys.get_unique(ctx), "_10");
assert_eq!(keys.get_unique(&mut ctx), "_11"); assert_eq!(keys.get_unique(ctx), "_11");
assert_eq!(keys.get_unique(&mut ctx), "_14"); assert_eq!(keys.get_unique(ctx), "_14");
} }
#[test] #[test]
@ -396,9 +399,9 @@ mod test {
keys.reserve("_4"); keys.reserve("_4");
keys.reserve("_"); keys.reserve("_");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_5"); assert_eq!(keys.get_unique(ctx), "_5");
assert_eq!(keys.get_unique(&mut ctx), "_6"); assert_eq!(keys.get_unique(ctx), "_6");
} }
#[test] #[test]
@ -410,8 +413,8 @@ mod test {
keys.reserve("_4"); keys.reserve("_4");
keys.reserve("_"); keys.reserve("_");
assert_eq!(keys.get_unique(&mut ctx), "_2"); assert_eq!(keys.get_unique(ctx), "_2");
assert_eq!(keys.get_unique(&mut ctx), "_3"); assert_eq!(keys.get_unique(ctx), "_3");
assert_eq!(keys.get_unique(&mut ctx), "_6"); assert_eq!(keys.get_unique(ctx), "_6");
} }
} }

View file

@ -35,7 +35,8 @@ const INITIAL_STACK_CAPACITY: usize = 64; // 64 entries = 1 KiB
/// 1. `TraverseAncestry`'s `stack` field is private. /// 1. `TraverseAncestry`'s `stack` field is private.
/// 2. Public methods of `TraverseAncestry` provide no means for mutating `stack`. /// 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: /// 3. Visitors receive a `&mut TraverseCtx`, but cannot overwrite its `ancestry` field because they:
/// a. cannot create a new `TraverseAncestry` - `TraverseAncestry::new` is private. /// a. cannot create a new `TraverseAncestry`
/// - `TraverseAncestry::new` and `TraverseCtx::new` are private.
/// b. cannot obtain an owned `TraverseAncestry` from a `&TraverseAncestry` /// b. cannot obtain an owned `TraverseAncestry` from a `&TraverseAncestry`
/// - `TraverseAncestry` is not `Clone`. /// - `TraverseAncestry` is not `Clone`.
pub struct TraverseAncestry<'a> { pub struct TraverseAncestry<'a> {

View file

@ -19,11 +19,13 @@ use crate::{
mod ancestry; mod ancestry;
mod bound_identifier; mod bound_identifier;
mod maybe_bound_identifier; mod maybe_bound_identifier;
mod reusable;
mod scoping; mod scoping;
use ancestry::PopToken; use ancestry::PopToken;
pub use ancestry::TraverseAncestry; pub use ancestry::TraverseAncestry;
pub use bound_identifier::BoundIdentifier; pub use bound_identifier::BoundIdentifier;
pub use maybe_bound_identifier::MaybeBoundIdentifier; pub use maybe_bound_identifier::MaybeBoundIdentifier;
pub use reusable::ReusableTraverseCtx;
pub use scoping::TraverseScoping; pub use scoping::TraverseScoping;
/// Traverse context. /// Traverse context.
@ -119,14 +121,6 @@ pub struct TraverseCtx<'a> {
// Public methods // Public methods
impl<'a> TraverseCtx<'a> { impl<'a> TraverseCtx<'a> {
/// Create new traversal context.
pub fn new(scopes: ScopeTree, symbols: SymbolTable, allocator: &'a Allocator) -> Self {
let ancestry = TraverseAncestry::new();
let scoping = TraverseScoping::new(scopes, symbols);
let ast = AstBuilder::new(allocator);
Self { ancestry, scoping, ast }
}
/// Allocate a node in the arena. /// Allocate a node in the arena.
/// ///
/// Returns a [`Box<T>`]. /// Returns a [`Box<T>`].
@ -601,6 +595,17 @@ impl<'a> TraverseCtx<'a> {
// Methods used internally within crate // Methods used internally within crate
impl<'a> TraverseCtx<'a> { impl<'a> TraverseCtx<'a> {
/// Create new traversal context.
///
/// # SAFETY
/// This function must not be public to maintain soundness of [`TraverseAncestry`].
pub(crate) fn new(scopes: ScopeTree, symbols: SymbolTable, allocator: &'a Allocator) -> Self {
let ancestry = TraverseAncestry::new();
let scoping = TraverseScoping::new(scopes, symbols);
let ast = AstBuilder::new(allocator);
Self { ancestry, scoping, ast }
}
/// Shortcut for `self.ancestry.push_stack`, to make `walk_*` methods less verbose. /// Shortcut for `self.ancestry.push_stack`, to make `walk_*` methods less verbose.
/// ///
/// # SAFETY /// # SAFETY

View file

@ -0,0 +1,56 @@
use oxc_allocator::Allocator;
use oxc_semantic::{ScopeTree, SymbolTable};
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 [`SymbolTable`] and [`ScopeTree`], which discards the sensitive [`TraverseAncestry`]
/// in the process.
///
/// [`TraverseAncestry`]: super::TraverseAncestry
#[repr(transparent)]
pub struct ReusableTraverseCtx<'a>(TraverseCtx<'a>);
// Public methods
impl<'a> ReusableTraverseCtx<'a> {
/// Create new [`ReusableTraverseCtx`].
pub fn new(scopes: ScopeTree, symbols: SymbolTable, allocator: &'a Allocator) -> Self {
Self(TraverseCtx::new(scopes, symbols, allocator))
}
/// Consume [`ReusableTraverseCtx`] and return [`SymbolTable`] and [`ScopeTree`].
pub fn into_symbol_table_and_scope_tree(self) -> (SymbolTable, ScopeTree) {
self.0.scoping.into_symbol_table_and_scope_tree()
}
/// Unwrap [`TraverseCtx`] in a [`ReusableTraverseCtx`].
///
/// Only for use in tests. Allows circumventing the safety invariants of [`TraverseAncestry`].
///
/// # SAFETY
/// Caller must ensure [`TraverseCtx`] returned by this method is not used unsoundly.
/// See [`TraverseAncestry`] for details of the invariants.
///
/// [`TraverseAncestry`]: super::TraverseAncestry
#[inline]
#[expect(clippy::missing_safety_doc)]
pub unsafe fn unwrap(self) -> TraverseCtx<'a> {
self.0
}
}
// Internal methods
impl<'a> ReusableTraverseCtx<'a> {
/// 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> {
&mut self.0
}
}

View file

@ -31,10 +31,6 @@ pub struct TraverseScoping {
// Public methods // Public methods
impl TraverseScoping { impl TraverseScoping {
pub fn into_symbol_table_and_scope_tree(self) -> (SymbolTable, ScopeTree) {
(self.symbols, self.scopes)
}
/// Get current scope ID /// Get current scope ID
#[inline] #[inline]
pub fn current_scope_id(&self) -> ScopeId { pub fn current_scope_id(&self) -> ScopeId {
@ -408,6 +404,11 @@ impl TraverseScoping {
} }
} }
/// Consume [`TraverseScoping`] and return [`SymbolTable`] and [`ScopeTree`].
pub(super) fn into_symbol_table_and_scope_tree(self) -> (SymbolTable, ScopeTree) {
(self.symbols, self.scopes)
}
/// Set current scope ID /// Set current scope ID
#[inline] #[inline]
pub(crate) fn set_current_scope_id(&mut self, scope_id: ScopeId) { pub(crate) fn set_current_scope_id(&mut self, scope_id: ScopeId) {

View file

@ -60,6 +60,8 @@
//! scheme could very easily be derailed entirely by a single mistake, so in my opinion, it's unwise //! scheme could very easily be derailed entirely by a single mistake, so in my opinion, it's unwise
//! to edit by hand. //! to edit by hand.
use std::ptr;
use oxc_allocator::Allocator; use oxc_allocator::Allocator;
use oxc_ast::ast::Program; use oxc_ast::ast::Program;
use oxc_semantic::{ScopeTree, SymbolTable}; use oxc_semantic::{ScopeTree, SymbolTable};
@ -67,7 +69,8 @@ use oxc_semantic::{ScopeTree, SymbolTable};
pub mod ast_operations; pub mod ast_operations;
mod context; mod context;
pub use context::{ pub use context::{
BoundIdentifier, MaybeBoundIdentifier, TraverseAncestry, TraverseCtx, TraverseScoping, BoundIdentifier, MaybeBoundIdentifier, ReusableTraverseCtx, TraverseAncestry, TraverseCtx,
TraverseScoping,
}; };
mod generated { mod generated {
@ -77,7 +80,7 @@ mod generated {
pub(super) mod walk; pub(super) mod walk;
} }
pub use generated::{ancestor, ancestor::Ancestor, traverse::Traverse}; pub use generated::{ancestor, ancestor::Ancestor, traverse::Traverse};
use generated::{scopes_collector, walk}; use generated::{scopes_collector, walk::walk_program};
mod compile_fail_tests; mod compile_fail_tests;
@ -144,7 +147,6 @@ mod compile_fail_tests;
/// } /// }
/// } /// }
/// ``` /// ```
pub fn traverse_mut<'a, Tr: Traverse<'a>>( pub fn traverse_mut<'a, Tr: Traverse<'a>>(
traverser: &mut Tr, traverser: &mut Tr,
allocator: &'a Allocator, allocator: &'a Allocator,
@ -152,17 +154,24 @@ pub fn traverse_mut<'a, Tr: Traverse<'a>>(
symbols: SymbolTable, symbols: SymbolTable,
scopes: ScopeTree, scopes: ScopeTree,
) -> (SymbolTable, ScopeTree) { ) -> (SymbolTable, ScopeTree) {
let mut ctx = TraverseCtx::new(scopes, symbols, allocator); let mut ctx = ReusableTraverseCtx::new(scopes, symbols, allocator);
walk_program(traverser, program, &mut ctx); traverse_mut_with_ctx(traverser, program, &mut ctx);
debug_assert!(ctx.ancestors_depth() == 1); ctx.into_symbol_table_and_scope_tree()
ctx.scoping.into_symbol_table_and_scope_tree()
} }
pub fn walk_program<'a, Tr: Traverse<'a>>( /// Traverse AST with a [`Traverse`] impl, reusing an existing [`ReusableTraverseCtx`].
///
/// [`ReusableTraverseCtx`] is specific to a single AST. It will likely cause malfunction if
/// `traverse_mut_with_ctx` is called with a [`Program`] and [`ReusableTraverseCtx`] which do not match.
///
/// See [`traverse_mut`] for more details of how traversal works.
pub fn traverse_mut_with_ctx<'a, Tr: Traverse<'a>>(
traverser: &mut Tr, traverser: &mut Tr,
program: &mut Program<'a>, program: &mut Program<'a>,
ctx: &mut TraverseCtx<'a>, ctx: &mut ReusableTraverseCtx<'a>,
) { ) {
let ctx = ctx.get_mut();
// SAFETY: Walk functions are constructed to avoid unsoundness // SAFETY: Walk functions are constructed to avoid unsoundness
unsafe { walk::walk_program(traverser, std::ptr::from_mut(program), ctx) }; unsafe { walk_program(traverser, ptr::from_mut(program), ctx) };
debug_assert!(ctx.ancestors_depth() == 1);
} }