-
-
Notifications
You must be signed in to change notification settings - Fork 604
fix(isolated_declarations): Emit computed properties when they are well known symbols #4099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
use std::cell::Cell; | ||
|
||
#[allow(clippy::wildcard_imports)] | ||
use oxc_ast::ast::*; | ||
#[allow(clippy::wildcard_imports)] | ||
use oxc_ast::{visit::walk::*, Visit}; | ||
use oxc_span::{Atom, GetSpan, Span}; | ||
use oxc_syntax::scope::{ScopeFlags, ScopeId}; | ||
use rustc_hash::FxHashSet; | ||
|
||
pub struct GlobalSymbolBindingTracker { | ||
depth: u8, | ||
symbol_binding_depth: Option<u8>, | ||
global_this_binding_depth: Option<u8>, | ||
computed_properties_using_non_global_symbol: FxHashSet<Span>, | ||
computed_properties_using_non_global_global_this: FxHashSet<Span>, | ||
} | ||
|
||
impl GlobalSymbolBindingTracker { | ||
pub fn new() -> Self { | ||
Self { | ||
depth: 0, | ||
symbol_binding_depth: None, | ||
global_this_binding_depth: None, | ||
computed_properties_using_non_global_symbol: FxHashSet::default(), | ||
computed_properties_using_non_global_global_this: FxHashSet::default(), | ||
} | ||
} | ||
|
||
fn does_computed_property_reference_non_global_symbol(&self, key: &PropertyKey) -> bool { | ||
self.computed_properties_using_non_global_symbol.contains(&key.span()) | ||
} | ||
|
||
fn does_computed_property_reference_non_global_global_this(&self, key: &PropertyKey) -> bool { | ||
self.computed_properties_using_non_global_global_this.contains(&key.span()) | ||
} | ||
|
||
pub fn does_computed_property_reference_well_known_symbol(&self, key: &PropertyKey) -> bool { | ||
if let PropertyKey::StaticMemberExpression(expr) = key { | ||
if let Expression::Identifier(identifier) = &expr.object { | ||
identifier.name == "Symbol" | ||
&& !self.does_computed_property_reference_non_global_symbol(key) | ||
} else if let Expression::StaticMemberExpression(static_member) = &expr.object { | ||
if let Expression::Identifier(identifier) = &static_member.object { | ||
identifier.name == "globalThis" | ||
&& static_member.property.name == "Symbol" | ||
&& !self.does_computed_property_reference_non_global_global_this(key) | ||
} else { | ||
false | ||
} | ||
} else { | ||
false | ||
} | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
fn handle_name_binding(&mut self, name: &Atom) { | ||
match name.as_str() { | ||
"Symbol" if self.symbol_binding_depth.is_none() => { | ||
self.symbol_binding_depth = Some(self.depth); | ||
} | ||
"globalThis" if self.global_this_binding_depth.is_none() => { | ||
self.global_this_binding_depth = Some(self.depth); | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
impl<'a> Visit<'a> for GlobalSymbolBindingTracker { | ||
fn enter_scope(&mut self, _: ScopeFlags, _: &Cell<Option<ScopeId>>) { | ||
self.depth += 1; | ||
} | ||
|
||
fn leave_scope(&mut self) { | ||
if self.symbol_binding_depth == Some(self.depth) { | ||
self.symbol_binding_depth = None; | ||
} | ||
if self.global_this_binding_depth == Some(self.depth) { | ||
self.global_this_binding_depth = None; | ||
} | ||
self.depth -= 1; | ||
} | ||
|
||
fn visit_ts_type(&mut self, _: &TSType<'a>) { | ||
// Optimization: we don't need to traverse down into types. | ||
MichaelMitchell-at marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
fn visit_statement(&mut self, statement: &Statement<'a>) { | ||
// Optimizations: Only try to visit parts of statements containing other statements. | ||
match statement { | ||
Statement::TSEnumDeclaration(_) | ||
| Statement::TSTypeAliasDeclaration(_) | ||
| Statement::TSInterfaceDeclaration(_) => (), | ||
Statement::WhileStatement(stmt) => walk_statement(self, &stmt.body), | ||
Statement::DoWhileStatement(stmt) => walk_statement(self, &stmt.body), | ||
Statement::ForStatement(stmt) => walk_statement(self, &stmt.body), | ||
Statement::ForInStatement(stmt) => walk_statement(self, &stmt.body), | ||
Statement::ForOfStatement(stmt) => walk_statement(self, &stmt.body), | ||
Statement::IfStatement(stmt) => { | ||
walk_statement(self, &stmt.consequent); | ||
if let Some(alt) = &stmt.alternate { | ||
walk_statement(self, alt); | ||
} | ||
} | ||
Statement::SwitchStatement(stmt) => { | ||
walk_switch_cases(self, &stmt.cases); | ||
} | ||
_ => walk_statement(self, statement), | ||
} | ||
} | ||
|
||
fn visit_binding_pattern(&mut self, pattern: &BindingPattern<'a>) { | ||
if let BindingPatternKind::BindingIdentifier(ident) = &pattern.kind { | ||
self.handle_name_binding(&ident.name); | ||
} | ||
walk_binding_pattern(self, pattern); | ||
} | ||
|
||
fn visit_assignment_pattern(&mut self, pattern: &AssignmentPattern<'a>) { | ||
// If the left side of the assignment already has a type annotation, we don't need to visit the right side. | ||
if pattern.left.type_annotation.is_some() { | ||
self.visit_binding_pattern(&pattern.left); | ||
return; | ||
} | ||
walk_assignment_pattern(self, pattern); | ||
} | ||
|
||
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) { | ||
// If the variable already has a type annotation, we don't need to visit the initializer. | ||
if decl.id.type_annotation.is_some() { | ||
self.visit_binding_pattern(&decl.id); | ||
} else { | ||
walk_variable_declarator(self, decl); | ||
} | ||
} | ||
|
||
fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'a>) { | ||
// If the left side of the assignment is a member expression, it won't affect emitted declarations. | ||
if expr.left.is_member_expression() { | ||
return; | ||
} | ||
walk_assignment_expression(self, expr); | ||
} | ||
|
||
fn visit_function(&mut self, func: &Function<'a>, flags: ScopeFlags) { | ||
// Async and generator functions always need explicit declarations. | ||
if func.generator || func.r#async { | ||
return; | ||
} | ||
|
||
if let Some(id) = func.id.as_ref() { | ||
self.handle_name_binding(&id.name); | ||
} | ||
|
||
// If the function already has a return type annotation, we don't need to visit the body. | ||
if func.return_type.is_some() { | ||
return; | ||
} | ||
|
||
walk_function(self, func, flags); | ||
} | ||
|
||
fn visit_arrow_function_expression(&mut self, func: &ArrowFunctionExpression<'a>) { | ||
// If the arrow function already has a return type annotation, we don't need to visit the body. | ||
if func.return_type.is_some() { | ||
return; | ||
} | ||
|
||
walk_arrow_function_expression(self, func); | ||
} | ||
|
||
fn visit_expression(&mut self, expr: &Expression<'a>) { | ||
match expr { | ||
Expression::ArrowFunctionExpression(_) | ||
| Expression::Identifier(_) | ||
| Expression::ArrayExpression(_) | ||
| Expression::AssignmentExpression(_) | ||
| Expression::ClassExpression(_) | ||
| Expression::FunctionExpression(_) | ||
| Expression::ObjectExpression(_) | ||
| Expression::ParenthesizedExpression(_) => { | ||
// Expressions whose types can be inferred, but excluding trivial ones | ||
// whose types will never contain a Symbol computed property. | ||
walk_expression(self, expr); | ||
} | ||
_ => (), | ||
} | ||
} | ||
|
||
fn visit_declaration(&mut self, declaration: &Declaration<'a>) { | ||
match declaration { | ||
Declaration::VariableDeclaration(_) | Declaration::FunctionDeclaration(_) => { | ||
// handled in BindingPattern and Function | ||
} | ||
Declaration::ClassDeclaration(decl) => { | ||
if let Some(id) = decl.id.as_ref() { | ||
self.handle_name_binding(&id.name); | ||
} | ||
} | ||
Declaration::TSModuleDeclaration(decl) => { | ||
if let TSModuleDeclarationName::Identifier(ident) = &decl.id { | ||
self.handle_name_binding(&ident.name); | ||
} | ||
} | ||
Declaration::TSImportEqualsDeclaration(decl) => { | ||
self.handle_name_binding(&decl.id.name); | ||
return; | ||
} | ||
Declaration::TSEnumDeclaration(decl) => { | ||
self.handle_name_binding(&decl.id.name); | ||
return; | ||
} | ||
Declaration::TSTypeAliasDeclaration(_) | Declaration::TSInterfaceDeclaration(_) => { | ||
return; | ||
} | ||
} | ||
walk_declaration(self, declaration); | ||
} | ||
|
||
fn visit_object_property(&mut self, prop: &ObjectProperty<'a>) { | ||
if prop.computed { | ||
if let PropertyKey::StaticMemberExpression(expr) = &prop.key { | ||
if self.symbol_binding_depth.is_some() { | ||
if let Expression::Identifier(identifier) = &expr.object { | ||
if identifier.name == "Symbol" { | ||
self.computed_properties_using_non_global_symbol.insert(expr.span); | ||
} | ||
} | ||
} | ||
|
||
if self.global_this_binding_depth.is_some() { | ||
if let Expression::StaticMemberExpression(static_member) = &expr.object { | ||
if let Expression::Identifier(identifier) = &static_member.object { | ||
if identifier.name == "globalThis" | ||
&& static_member.property.name == "Symbol" | ||
{ | ||
self.computed_properties_using_non_global_global_this | ||
.insert(expr.span); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
walk_object_property(self, prop); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ mod diagnostics; | |
mod r#enum; | ||
mod formal_parameter_binding_pattern; | ||
mod function; | ||
mod global_symbol_binding_tracker; | ||
mod inferrer; | ||
mod literal; | ||
mod module; | ||
|
@@ -29,6 +30,7 @@ use oxc_diagnostics::OxcDiagnostic; | |
use oxc_span::{Atom, GetSpan, SourceType, SPAN}; | ||
use rustc_hash::{FxHashMap, FxHashSet}; | ||
|
||
use crate::global_symbol_binding_tracker::GlobalSymbolBindingTracker; | ||
use crate::scope::ScopeTree; | ||
|
||
#[derive(Debug, Clone, Copy)] | ||
|
@@ -49,6 +51,7 @@ pub struct IsolatedDeclarations<'a> { | |
|
||
// state | ||
scope: ScopeTree<'a>, | ||
global_symbol_binding_tracker: GlobalSymbolBindingTracker, | ||
errors: RefCell<Vec<OxcDiagnostic>>, | ||
|
||
// options | ||
|
@@ -75,6 +78,7 @@ impl<'a> IsolatedDeclarations<'a> { | |
strip_internal, | ||
internal_annotations: is_internal_set, | ||
scope: ScopeTree::new(allocator), | ||
global_symbol_binding_tracker: GlobalSymbolBindingTracker::new(), | ||
errors: RefCell::new(vec![]), | ||
} | ||
} | ||
|
@@ -126,6 +130,10 @@ impl<'a> IsolatedDeclarations<'a> { | |
&mut self, | ||
program: &Program<'a>, | ||
) -> oxc_allocator::Vec<'a, Statement<'a>> { | ||
// Collect information about global Symbol usage within computed | ||
// properties before performing any transformations. | ||
self.global_symbol_binding_tracker.visit_program(program); | ||
Comment on lines
+133
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably avoid allocations for this most of the time by first checking if there is both an occurrence of overriding the global Symbol and references to Symbol anywhere in the program, not necessarily in the same scope, but I'm not sure what the cost of that extra traversal would be. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to check on demand? For example, we initially only check for the root scope. When we transform the function we will check the function. This will only check for declarations that will be exported There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly, I will think about it some more. I think one of the main challenges comes just from organizing the code and performing state management. It feels like similar to with (Just voicing my thoughts out loud) I think what feels redundant with my current approach is that the short-circuit optimizations I take are making assumptions about how the tree will be traversed for emitting declarations, so that logic kind of feels like it's being reproduced in two places. The code could be potentially cleaner if that logic could be pulled out. What I imagine is something like alternative Please let me know your thoughts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Only
Adding another trait instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's a good point. I suppose it would be necessary for such a trait to extend
I may not be able to get to this soon, but as you said, we have time! |
||
|
||
let has_import_or_export = program.body.iter().any(|stmt| { | ||
matches!( | ||
stmt, | ||
|
Uh oh!
There was an error while loading. Please reload this page.