perf(isolated-declarations): replace hash collections with index-based Vec (#19221)

## Summary

- Eliminate redundant `transformed_spans: FxHashSet<Span>` by consolidating passthrough cases into `transformed_stmts`
- Replace `transformed_stmts: FxHashMap<Span, Statement>` with `Vec<Option<Statement>>` indexed by statement position for O(1) lookups
- Merge `get_assignable_properties_for_namespaces` into `report_error_for_expando_function` as a single forward pass

**Benchmark (vue-id.ts):** 8.53ms → 6.57ms (**~23% faster**)

## Test plan

- [x] `cargo test -p oxc_isolated_declarations` — all 20 tests pass
- [x] `cargo clippy` — no warnings
- [x] `cargo bench -p oxc_benchmark --bench transformer -- isolated-declarations`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Dunqing committed 2026-02-11 13:11:37 +00:00
1 parent 60d242a9b2
commit e5baf60fbf
2 files changed
+96 -117

No files matched your search

@@ -29,12 +29,10 @@ impl<'a> IsolatedDeclarations<'a> {
"undefined" => Some(self.ast.ts_type_undefined_keyword(SPAN)),
_ => None,
},
Expression::FunctionExpression(func) => {
self.transform_function_to_ts_type(func).map(|x| x.clone_in(self.ast.allocator))
Expression::FunctionExpression(func) => self.transform_function_to_ts_type(func),
Expression::ArrowFunctionExpression(func) => {
self.transform_arrow_function_to_ts_type(func)
}
Expression::ArrowFunctionExpression(func) => self
.transform_arrow_function_to_ts_type(func)
.map(|x| x.clone_in(self.ast.allocator)),
Expression::ObjectExpression(expr) => {
Some(self.transform_object_expression_to_ts_type(expr, false))
}
+93 -112
View File
@@ -5,7 +5,7 @@
//! * <https://www.typescriptlang.org/tsconfig#isolatedDeclarations>
//! * <https://github.com/microsoft/TypeScript/blob/v5.6.3/src/compiler/transformers/declarations.ts>
use std::{cell::RefCell, mem};
use std::{cell::RefCell, iter::repeat_with, mem};
use rustc_hash::{FxHashMap, FxHashSet};
@@ -183,9 +183,9 @@ impl<'a> IsolatedDeclarations<'a> {
// https://github.com/microsoft/TypeScript/pull/58912
let mut need_empty_export_marker = true;
// Use span to identify transformed nodes
let mut transformed_spans: FxHashSet<Span> = FxHashSet::default();
let mut transformed_stmts: FxHashMap<Span, Statement<'a>> = FxHashMap::default();
let mut transformed_stmts: Vec<Option<Statement<'a>>> =
repeat_with(|| None).take(stmts.len()).collect();
let mut transformed_count: usize = 0;
let mut transformed_variable_declarator: FxHashMap<Span, VariableDeclarator<'a>> =
FxHashMap::default();
// When transforming `export default` with expression or `export = expression`,
@@ -196,15 +196,13 @@ impl<'a> IsolatedDeclarations<'a> {
// 2. Transform export declarations
// 3. Collect all bindings / reference from module declarations
// 4. Collect transformed indexes
for &stmt in &stmts {
for (idx, &stmt) in stmts.iter().enumerate() {
match stmt {
match_declaration!(Statement) => {
if let Statement::TSModuleDeclaration(decl) = stmt {
// `declare module "foo" { ... }`
// We need to emit it anyway
if decl.id.is_string_literal() {
transformed_spans.insert(decl.span);
let mut decl = decl.clone_in(self.ast.allocator);
// Remove export keyword from all statements in `declare module "xxx" { ... }`
if let Some(body) =
@@ -216,25 +214,23 @@ impl<'a> IsolatedDeclarations<'a> {
// We need to visit the module declaration to collect all references
self.scope.visit_ts_module_declaration(decl.as_ref());
transformed_stmts
.insert(decl.span, Statement::TSModuleDeclaration(decl));
transformed_stmts[idx] = Some(Statement::TSModuleDeclaration(decl));
transformed_count += 1;
}
} else if let Statement::TSGlobalDeclaration(decl) = stmt {
// `declare global { ... }`
// We need to emit it anyway
transformed_spans.insert(decl.span);
let decl = decl.clone_in(self.ast.allocator);
// We need to visit the module declaration to collect all references
self.scope.visit_ts_global_declaration(decl.as_ref());
transformed_stmts.insert(decl.span, Statement::TSGlobalDeclaration(decl));
transformed_stmts[idx] = Some(Statement::TSGlobalDeclaration(decl));
transformed_count += 1;
}
}
match_module_declaration!(Statement) => {
match stmt.to_module_declaration() {
ModuleDeclaration::TSExportAssignment(decl) => {
transformed_spans.insert(decl.span);
if let Some((var_decl, new_decl)) =
self.transform_ts_export_assignment(decl)
{
@@ -244,14 +240,15 @@ impl<'a> IsolatedDeclarations<'a> {
}
self.scope.visit_statement(&new_decl);
transformed_stmts.insert(decl.span, new_decl);
transformed_stmts[idx] = Some(new_decl);
} else {
self.scope.visit_ts_export_assignment(decl);
transformed_stmts[idx] = Some(stmt.clone_in(self.ast.allocator));
}
transformed_count += 1;
need_empty_export_marker = false;
}
ModuleDeclaration::ExportDefaultDeclaration(decl) => {
transformed_spans.insert(decl.span);
if let Some((var_decl, new_decl)) =
self.transform_export_default_declaration(decl)
{
@@ -261,35 +258,40 @@ impl<'a> IsolatedDeclarations<'a> {
}
self.scope.visit_statement(&new_decl);
transformed_stmts.insert(decl.span, new_decl);
transformed_stmts[idx] = Some(new_decl);
} else {
self.scope.visit_export_default_declaration(decl);
transformed_stmts[idx] = Some(stmt.clone_in(self.ast.allocator));
}
transformed_count += 1;
need_empty_export_marker = false;
}
ModuleDeclaration::ExportNamedDeclaration(decl) => {
transformed_spans.insert(decl.span);
if let Some(new_decl) = self.transform_export_named_declaration(decl) {
self.scope.visit_export_named_declaration(&new_decl);
transformed_stmts.insert(
decl.span,
Statement::from(ModuleDeclaration::ExportNamedDeclaration(
transformed_stmts[idx] = Some(Statement::from(
ModuleDeclaration::ExportNamedDeclaration(
self.ast.alloc(new_decl),
)),
);
),
));
} else if decl.declaration.is_none() {
need_empty_export_marker = false;
self.scope.visit_export_named_declaration(decl);
transformed_stmts[idx] = Some(stmt.clone_in(self.ast.allocator));
} else {
// Declaration couldn't be transformed; preserve as-is
transformed_stmts[idx] = Some(stmt.clone_in(self.ast.allocator));
}
transformed_count += 1;
}
ModuleDeclaration::ImportDeclaration(_) => {
// We must transform this in the end, because we need to know all references
}
module_declaration => {
transformed_spans.insert(module_declaration.span());
self.scope.visit_module_declaration(module_declaration);
transformed_stmts[idx] = Some(stmt.clone_in(self.ast.allocator));
transformed_count += 1;
}
}
}
@@ -297,26 +299,21 @@ impl<'a> IsolatedDeclarations<'a> {
}
}
let last_transformed_len = transformed_spans.len() + transformed_variable_declarator.len();
let last_transformed_len = transformed_count + transformed_variable_declarator.len();
// 5. Transform statements until no more transformation can be done
loop {
let cur_transformed_len =
transformed_spans.len() + transformed_variable_declarator.len();
for stmt in &stmts {
if transformed_spans.contains(&stmt.span()) {
let mut changed = true;
while changed {
changed = false;
for (idx, stmt) in stmts.iter().enumerate() {
if transformed_stmts[idx].is_some() {
continue;
}
let Some(decl) = stmt.as_declaration() else { continue };
// Skip transformed declarations
if transformed_spans.contains(&decl.span()) {
continue;
}
if let Declaration::VariableDeclaration(declaration) = decl {
let mut all_declarator_has_transformed = true;
for declarator in &declaration.declarations {
if transformed_spans.contains(&declarator.span) {
if transformed_variable_declarator.contains_key(&declarator.span) {
continue;
}
@@ -325,6 +322,7 @@ impl<'a> IsolatedDeclarations<'a> {
{
self.scope.visit_variable_declarator(&new_declarator);
transformed_variable_declarator.insert(declarator.span, new_declarator);
changed = true;
} else {
all_declarator_has_transformed = false;
}
@@ -341,29 +339,21 @@ impl<'a> IsolatedDeclarations<'a> {
declarations,
self.is_declare(),
);
transformed_stmts.insert(
declaration.span,
Statement::VariableDeclaration(self.ast.alloc(decl)),
);
transformed_spans.insert(declaration.span);
transformed_stmts[idx] =
Some(Statement::VariableDeclaration(self.ast.alloc(decl)));
transformed_count += 1;
}
} else if let Some(new_decl) = self.transform_declaration(decl, true) {
self.scope.visit_declaration(&new_decl);
transformed_spans.insert(new_decl.span());
transformed_stmts.insert(new_decl.span(), Statement::from(new_decl));
transformed_stmts[idx] = Some(Statement::from(new_decl));
transformed_count += 1;
changed = true;
}
}
// Use transformed_spans to weather we need to continue
if cur_transformed_len
== transformed_spans.len() + transformed_variable_declarator.len()
{
break;
}
}
// If any declaration is transformed in previous step, we don't need to add empty export marker
if last_transformed_len != 0 && last_transformed_len == transformed_spans.len() {
if last_transformed_len != 0 && last_transformed_len == transformed_count {
need_empty_export_marker = false;
}
@@ -373,11 +363,8 @@ impl<'a> IsolatedDeclarations<'a> {
+ usize::from(extra_export_var_statement.is_some())
+ usize::from(need_empty_export_marker),
);
for stmt in stmts {
if transformed_spans.contains(&stmt.span()) {
let new_stmt = transformed_stmts
.remove(&stmt.span())
.unwrap_or_else(|| stmt.clone_in(self.ast.allocator));
for (idx, stmt) in stmts.into_iter().enumerate() {
if let Some(new_stmt) = transformed_stmts[idx].take() {
if matches!(
new_stmt,
Statement::ExportDefaultDeclaration(_) | Statement::TSExportAssignment(_)
@@ -505,76 +492,58 @@ impl<'a> IsolatedDeclarations<'a> {
});
}
fn get_assignable_properties_for_namespaces(
stmts: &'a ArenaVec<'a, Statement<'a>>,
) -> FxHashMap<&'a str, FxHashSet<Atom<'a>>> {
let mut assignable_properties_for_namespace = FxHashMap::<&str, FxHashSet<Atom>>::default();
for stmt in stmts {
let decl = match stmt {
Statement::ExportNamedDeclaration(decl) => {
if let Some(Declaration::TSModuleDeclaration(decl)) = &decl.declaration {
decl
} else {
continue;
}
}
Statement::TSModuleDeclaration(decl) => decl,
_ => continue,
};
if decl.kind != TSModuleDeclarationKind::Namespace {
continue;
}
let TSModuleDeclarationName::Identifier(ident) = &decl.id else { continue };
let Some(TSModuleDeclarationBody::TSModuleBlock(block)) = &decl.body else {
continue;
};
for stmt in &block.body {
let Statement::ExportNamedDeclaration(decl) = stmt else { continue };
match &decl.declaration {
Some(Declaration::VariableDeclaration(var)) => {
for declarator in &var.declarations {
if let Some(name) = declarator.id.get_identifier_name() {
assignable_properties_for_namespace
.entry(ident.name.as_str())
.or_default()
.insert(name.into());
}
}
}
Some(Declaration::FunctionDeclaration(func)) => {
if let Some(name) = func.name() {
assignable_properties_for_namespace
/// Collect exported names from a namespace declaration into `assignable_properties`.
fn collect_namespace_properties(
decl: &TSModuleDeclaration<'a>,
assignable_properties: &mut FxHashMap<&'a str, FxHashSet<Atom<'a>>>,
) {
if decl.kind != TSModuleDeclarationKind::Namespace {
return;
}
let TSModuleDeclarationName::Identifier(ident) = &decl.id else { return };
let Some(TSModuleDeclarationBody::TSModuleBlock(block)) = &decl.body else { return };
for stmt in &block.body {
let Statement::ExportNamedDeclaration(decl) = stmt else { continue };
match &decl.declaration {
Some(Declaration::VariableDeclaration(var)) => {
for declarator in &var.declarations {
if let Some(name) = declarator.id.get_identifier_name() {
assignable_properties
.entry(ident.name.as_str())
.or_default()
.insert(name.into());
}
}
Some(Declaration::ClassDeclaration(cls)) => {
if let Some(id) = cls.id.as_ref() {
assignable_properties_for_namespace
.entry(ident.name.as_str())
.or_default()
.insert(id.name.into());
}
}
Some(Declaration::TSEnumDeclaration(decl)) => {
assignable_properties_for_namespace
}
Some(Declaration::FunctionDeclaration(func)) => {
if let Some(name) = func.name() {
assignable_properties
.entry(ident.name.as_str())
.or_default()
.insert(decl.id.name.into());
.insert(name.into());
}
_ => {}
}
Some(Declaration::ClassDeclaration(cls)) => {
if let Some(id) = cls.id.as_ref() {
assignable_properties
.entry(ident.name.as_str())
.or_default()
.insert(id.name.into());
}
}
Some(Declaration::TSEnumDeclaration(decl)) => {
assignable_properties
.entry(ident.name.as_str())
.or_default()
.insert(decl.id.name.into());
}
_ => {}
}
}
assignable_properties_for_namespace
}
fn report_error_for_expando_function(&self, stmts: &ArenaVec<'a, Statement<'a>>) {
let assignable_properties_for_namespace =
IsolatedDeclarations::get_assignable_properties_for_namespaces(stmts);
let mut assignable_properties_for_namespace = FxHashMap::<&str, FxHashSet<Atom>>::default();
let mut can_expando_function_names = IdentHashSet::default();
for stmt in stmts {
match stmt {
@@ -596,6 +565,12 @@ impl<'a> IsolatedDeclarations<'a> {
}
}
}
Some(Declaration::TSModuleDeclaration(decl)) => {
Self::collect_namespace_properties(
decl,
&mut assignable_properties_for_namespace,
);
}
_ => (),
},
Statement::ExportDefaultDeclaration(decl) => {
@@ -626,6 +601,12 @@ impl<'a> IsolatedDeclarations<'a> {
}
}
}
Statement::TSModuleDeclaration(decl) => {
Self::collect_namespace_properties(
decl,
&mut assignable_properties_for_namespace,
);
}
Statement::ExpressionStatement(stmt) => {
if let Expression::AssignmentExpression(assignment) = &stmt.expression
&& let AssignmentTarget::StaticMemberExpression(static_member_expr) =