Skip to content
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

fix(isolated_declarations): Emit computed properties when they are well known symbols #4099

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions crates/oxc_isolated_declarations/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ impl<'a> IsolatedDeclarations<'a> {

pub fn report_property_key(&self, key: &PropertyKey<'a>, computed: bool) -> bool {
if computed && !self.is_literal_key(key) {
self.error(computed_property_name(key.span()));
true
if self
.global_symbol_binding_tracker
.does_computed_property_reference_well_known_symbol(key)
{
false
} else {
self.error(computed_property_name(key.span()));
true
}
} else {
false
}
Expand Down Expand Up @@ -281,19 +288,59 @@ impl<'a> IsolatedDeclarations<'a> {
/// 1. If it has no parameter, create a parameter with the name `value`
/// 2. If it has no parameter type, infer it from the getter method's return type
fn transform_getter_or_setter_methods(&self, decl: &mut Class<'a>) {
let mut method_annotations: FxHashMap<_, (bool, _, _)> = FxHashMap::default();
let mut method_annotations_for_static_name: FxHashMap<_, (bool, _, _)> =
FxHashMap::default();
let mut method_annotations_for_well_known_symbol: FxHashMap<_, (bool, _, _)> =
FxHashMap::default();

for element in decl.body.body.iter_mut() {
if let ClassElement::MethodDefinition(method) = element {
if method.key.is_private_identifier()
&& (method.computed && !self.is_literal_key(&method.key))
{
if method.key.is_private_identifier() {
continue;
}

let Some(name) = method.key.static_name() else {
let Some((name, is_static)) =
method.key.static_name().map(|name| (name, true)).or_else(|| {
if let PropertyKey::StaticMemberExpression(expr) = &method.key {
match &expr.object {
Expression::Identifier(object_identifier) => {
if object_identifier.name == "Symbol" {
Some((expr.property.name.clone().into(), false))
} else {
None
}
}
Expression::StaticMemberExpression(static_member) => {
if let Expression::Identifier(identifier) =
&static_member.object
{
if identifier.name == "globalThis"
&& static_member.property.name == "Symbol"
{
Some((expr.property.name.clone().into(), false))
} else {
None
}
} else {
None
}
}
_ => None,
}
} else {
None
}
})
else {
continue;
};

let method_annotations = if is_static {
&mut method_annotations_for_static_name
} else {
&mut method_annotations_for_well_known_symbol
};

match method.kind {
MethodDefinitionKind::Set => {
let params = &mut method.value.params;
Expand Down Expand Up @@ -328,7 +375,10 @@ impl<'a> IsolatedDeclarations<'a> {
}
}

for (requires_inference, param, return_type) in method_annotations.into_values() {
for (requires_inference, param, return_type) in method_annotations_for_static_name
.into_values()
.chain(method_annotations_for_well_known_symbol.into_values())
{
if requires_inference {
if let (Some(Some(annotation)), Some(option))
| (Some(option), Some(Some(annotation))) = (param, return_type)
Expand Down
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>,
MichaelMitchell-at marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
}
8 changes: 8 additions & 0 deletions crates/oxc_isolated_declarations/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand All @@ -49,6 +51,7 @@ pub struct IsolatedDeclarations<'a> {

// state
scope: ScopeTree<'a>,
global_symbol_binding_tracker: GlobalSymbolBindingTracker,
errors: RefCell<Vec<OxcDiagnostic>>,

// options
Expand All @@ -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![]),
}
}
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

@Dunqing Dunqing Jul 24, 2024

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ScopeTree we would need to tell GlobalSymbolBindingTracker when it is entering or leaving a scope, but the logic will diverge from ScopeTree (otherwise we would have just used ScopeTree for this purpose), which may result in the code looking more confusing.

(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 Visitor traits that only traverse the parts of the AST relevant to emitted declarations. Such traits would already have the "optimizations" I wrote naturally built-in. What's harder for me to imagine right now is how easy it would be to re-implement declaration transforms on top of such traits.

Please let me know your thoughts.

Copy link
Member

Choose a reason for hiding this comment

The 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 ScopeTree we would need to tell GlobalSymbolBindingTracker when it is entering or leaving a scope, but the logic will diverge from ScopeTree (otherwise we would have just used ScopeTree for this purpose), which may result in the code looking more confusing.

Only Function and TSModuleDeclaration will have nested scopes, which I don't think should be too much trouble. We could try.

(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 Visitor traits that only traverse the parts of the AST relevant to emitted declarations. Such traits would already have the "optimizations" I wrote naturally built-in. What's harder for me to imagine right now is how easy it would be to re-implement declaration transforms on top of such traits.

Adding another trait instead of Visitor may significantly increase maintenance costs. Currently Visitor is automatically generated with ast-codegen

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding another trait instead of Visitor may significantly increase maintenance costs

That's a good point. I suppose it would be necessary for such a trait to extend Visit and only override a small subset of the method implementations.

Only Function and TSModuleDeclaration will have nested scopes, which I don't think should be too much trouble. We could try.

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,
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_isolated_declarations/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl<'a> IsolatedDeclarations<'a> {

let property_signature = self.ast.ts_signature_property_signature(
object.span,
false,
object.computed,
false,
is_const,
// SAFETY: `ast.copy` is unsound! We need to fix.
Expand Down
Loading