From 6cbc5dd75be754a55aeed2f45a1f7dedf6a97d92 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 3 Dec 2023 21:35:44 -0800 Subject: [PATCH] feat(transformer): Start on `function_name` transform. (#1510) Co-authored-by: Boshen --- crates/oxc_ast/src/ast/js.rs | 4 + crates/oxc_transformer/Cargo.toml | 1 - .../src/es2015/function_name.rs | 172 ++++++++++++++++++ crates/oxc_transformer/src/es2015/mod.rs | 2 + crates/oxc_transformer/src/lib.rs | 41 ++++- crates/oxc_transformer/src/options.rs | 1 + justfile | 4 + tasks/transform_conformance/src/test_case.rs | 1 + 8 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 crates/oxc_transformer/src/es2015/function_name.rs diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 74ec6c5ef..081bfac21 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -445,6 +445,10 @@ impl<'a> PropertyKey<'a> { self.static_name().is_some_and(|n| n == name) } + pub fn is_identifier(&self) -> bool { + matches!(self, Self::PrivateIdentifier(_) | Self::Identifier(_)) + } + pub fn is_private_identifier(&self) -> bool { matches!(self, Self::PrivateIdentifier(_)) } diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index b34eb8cc5..30f42ac19 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -27,7 +27,6 @@ oxc_semantic = { workspace = true } oxc_diagnostics = { workspace = true } rustc-hash = { workspace = true } - serde = { workspace = true, features = ["derive"] } phf = { workspace = true, features = ["macros"] } diff --git a/crates/oxc_transformer/src/es2015/function_name.rs b/crates/oxc_transformer/src/es2015/function_name.rs new file mode 100644 index 000000000..d6ede0b07 --- /dev/null +++ b/crates/oxc_transformer/src/es2015/function_name.rs @@ -0,0 +1,172 @@ +use std::rc::Rc; + +// use lazy_static::lazy_static; +use oxc_ast::{ast::*, AstBuilder, Visit}; +use oxc_span::{Atom, Span}; +use oxc_syntax::operator::AssignmentOperator; +use oxc_syntax::unicode_id_start::is_id_continue; +// use regex::Regex; + +use crate::context::TransformerCtx; +use crate::options::TransformOptions; +use crate::utils::is_valid_identifier; + +/// ES2015: Function Name +/// +/// References: +/// * +/// * +pub struct FunctionName<'a> { + _ast: Rc>, + ctx: TransformerCtx<'a>, + unicode_escapes: bool, +} + +impl<'a> FunctionName<'a> { + pub fn new( + _ast: &Rc>, + _ctx: &TransformerCtx<'a>, + _options: &TransformOptions, + ) -> Option { + // Disabled for now + None + // (options.target < TransformTarget::ES2015 || options.function_name).then(|| Self { + // ast, + // ctx, + // // TODO hook up the plugin + // unicode_escapes: true, + // }) + } + + pub fn transform_assignment_expression(&mut self, expr: &mut AssignmentExpression<'a>) { + if expr.right.is_function() && matches!(expr.operator, AssignmentOperator::Assign) { + if let AssignmentTarget::SimpleAssignmentTarget( + SimpleAssignmentTarget::AssignmentTargetIdentifier(target), + ) = &expr.left + { + if let Some(id) = + create_valid_identifier(target.span, target.name.clone(), self.unicode_escapes) + { + self.transform_expression(&mut expr.right, id); + } + } + } + } + + pub fn transform_object_expression(&mut self, expr: &mut ObjectExpression<'a>) { + for property_kind in expr.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(property) = property_kind { + if property.computed || !property.value.is_function() { + continue; + } + + let id = match &property.key { + PropertyKey::Identifier(ident) => create_valid_identifier( + ident.span, + ident.name.clone(), + self.unicode_escapes, + ), + PropertyKey::PrivateIdentifier(ident) => create_valid_identifier( + ident.span, + ident.name.clone(), + self.unicode_escapes, + ), + PropertyKey::Expression(_) => continue, + }; + + if let Some(id) = id { + self.transform_expression(&mut property.value, id); + } + } + } + } + + pub fn transform_variable_declarator(&mut self, decl: &mut VariableDeclarator<'a>) { + let Some(init) = &mut decl.init else { return }; + + if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind { + // Create a new ID instead of cloning to avoid local binding/refs + if let Some(id) = + create_valid_identifier(ident.span, ident.name.clone(), self.unicode_escapes) + { + self.transform_expression(init, id); + } + }; + } + + // Internal only + fn transform_expression(&mut self, expr: &mut Expression<'a>, mut id: BindingIdentifier) { + // function () {} -> function name() {} + if let Expression::FunctionExpression(func) = expr { + let scopes = self.ctx.scopes(); + let mut count = 0; + + // let mut finder = IdentFinder { id, found: 0 }; + // finder.visit_expression(expr); + + // Check for nested params/vars of the same name + for scope in scopes.descendants() { + for binding in scopes.get_bindings(scope) { + if binding.0 == &id.name { + count += 1; + } + } + } + + // If we're shadowing, change the name + if count > 0 { + id.name = Atom::from(format!("{}{}", id.name, count)); + } + + if func.id.is_none() { + func.id = Some(id); + } + } + } +} + +struct IdentFinder { + id: BindingIdentifier, + found: usize, +} + +impl<'a> Visit<'a> for IdentFinder { + fn visit_binding_identifier(&mut self, ident: &BindingIdentifier) { + if ident.name == self.id.name { + self.found += 1; + } + } +} + +// https://github.com/babel/babel/blob/main/packages/babel-helper-function-name/src/index.ts +// https://github.com/babel/babel/blob/main/packages/babel-types/src/converters/toBindingIdentifierName.ts#L3 +// https://github.com/babel/babel/blob/main/packages/babel-types/src/converters/toIdentifier.ts#L4 +#[allow(clippy::unnecessary_wraps)] +fn create_valid_identifier( + span: Span, + atom: Atom, + _unicode_escapes: bool, +) -> Option { + // NOTE: this regex fails to compile on Rust + // lazy_static! { + // static ref UNICODE_NAME: Regex = Regex::new(r"(?u)[\u{D800}-\u{DFFF}]").unwrap(); + // } + + // if !unicode_escapes && UNICODE_NAME.is_match(atom.as_str()) { + // return None; + // } + + let id = Atom::from( + atom.chars().map(|c| if is_id_continue(c) { c } else { '-' }).collect::(), + ); + + let id = if id == "" { + Atom::from("_") + } else if id == "eval" || id == "arguments" || id == "null" || !is_valid_identifier(&id, true) { + Atom::from(format!("_{id}")) + } else { + atom + }; + + Some(BindingIdentifier::new(span, id)) +} diff --git a/crates/oxc_transformer/src/es2015/mod.rs b/crates/oxc_transformer/src/es2015/mod.rs index 9456cb146..bfc4f904e 100644 --- a/crates/oxc_transformer/src/es2015/mod.rs +++ b/crates/oxc_transformer/src/es2015/mod.rs @@ -1,5 +1,7 @@ +mod function_name; mod shorthand_properties; mod template_literals; +pub use function_name::FunctionName; pub use shorthand_properties::ShorthandProperties; pub use template_literals::TemplateLiterals; diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 32ea4d48c..52bfc1db0 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -33,7 +33,7 @@ use oxc_semantic::Semantic; use oxc_span::SourceType; use crate::{ - context::TransformerCtx, es2015::ShorthandProperties, es2016::ExponentiationOperator, + context::TransformerCtx, es2015::*, es2016::ExponentiationOperator, es2019::OptionalCatchBinding, es2020::NullishCoalescingOperator, es2021::LogicalAssignmentOperators, es2022::ClassStaticBlock, es3::PropertyLiteral, react_jsx::ReactJsx, regexp::RegexpFlags, typescript::TypeScript, utils::CreateVars, @@ -62,6 +62,7 @@ pub struct Transformer<'a> { // es2016 es2016_exponentiation_operator: Option>, // es2015 + es2015_function_name: Option>, es2015_shorthand_properties: Option>, es2015_template_literals: Option>, es3_property_literal: Option>, @@ -86,14 +87,22 @@ impl<'a> Transformer<'a> { // TODO: pass verbatim_module_syntax from user config typescript: source_type.is_typescript().then(|| TypeScript::new(Rc::clone(&ast), ctx.clone(), false)), regexp_flags: RegexpFlags::new(Rc::clone(&ast), &options), + // es2022 es2022_class_static_block: es2022::ClassStaticBlock::new(Rc::clone(&ast), &options), + // es2021 es2021_logical_assignment_operators: LogicalAssignmentOperators::new(Rc::clone(&ast), ctx.clone(), &options), + // es2020 es2020_nullish_coalescing_operators: NullishCoalescingOperator::new(Rc::clone(&ast), ctx.clone(), &options), + // es2019 es2019_optional_catch_binding: OptionalCatchBinding::new(Rc::clone(&ast), &options), + // es2016 es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), ctx.clone(), &options), + // es2015 + es2015_function_name: FunctionName::new(&ast, &ctx.clone(), &options), es2015_shorthand_properties: ShorthandProperties::new(Rc::clone(&ast), &options), es2015_template_literals: TemplateLiterals::new(Rc::clone(&ast), &options), - es3_property_literal: PropertyLiteral::new(Rc::clone(&ast ), &options), + // other + es3_property_literal: PropertyLiteral::new(Rc::clone(&ast), &options), react_jsx: ReactJsx::new(Rc::clone(&ast), ctx.clone(), options) } } @@ -129,6 +138,13 @@ impl<'a> VisitMut<'a> for Transformer<'a> { self.react_jsx.as_mut().map(|t| t.add_react_jsx_runtime_imports(program)); } + fn visit_assignment_expression(&mut self, expr: &mut AssignmentExpression<'a>) { + self.es2015_function_name.as_mut().map(|t| t.transform_assignment_expression(expr)); + + self.visit_assignment_target(&mut expr.left); + self.visit_expression(&mut expr.right); + } + fn visit_statements(&mut self, stmts: &mut oxc_allocator::Vec<'a, Statement<'a>>) { for stmt in stmts.iter_mut() { self.visit_statement(stmt); @@ -171,12 +187,21 @@ impl<'a> VisitMut<'a> for Transformer<'a> { self.visit_statements(&mut clause.body.body); } + fn visit_object_expression(&mut self, expr: &mut ObjectExpression<'a>) { + self.es2015_function_name.as_mut().map(|t| t.transform_object_expression(expr)); + + for property in expr.properties.iter_mut() { + self.visit_object_property_kind(property); + } + } + fn visit_object_property(&mut self, prop: &mut ObjectProperty<'a>) { self.es2015_shorthand_properties.as_mut().map(|t| t.transform_object_property(prop)); self.es3_property_literal.as_mut().map(|t| t.transform_object_property(prop)); self.visit_property_key(&mut prop.key); self.visit_expression(&mut prop.value); + if let Some(init) = &mut prop.init { self.visit_expression(init); } @@ -192,11 +217,23 @@ impl<'a> VisitMut<'a> for Transformer<'a> { fn visit_formal_parameters(&mut self, params: &mut FormalParameters<'a>) { self.typescript.as_mut().map(|t| t.transform_formal_parameters(params)); + for param in params.items.iter_mut() { self.visit_formal_parameter(param); } + if let Some(rest) = &mut params.rest { self.visit_rest_element(rest); } } + + fn visit_variable_declarator(&mut self, declarator: &mut VariableDeclarator<'a>) { + self.es2015_function_name.as_mut().map(|t| t.transform_variable_declarator(declarator)); + + self.visit_binding_pattern(&mut declarator.id); + + if let Some(init) = &mut declarator.init { + self.visit_expression(init); + } + } } diff --git a/crates/oxc_transformer/src/options.rs b/crates/oxc_transformer/src/options.rs index cd7b28b74..cfd8f1d77 100644 --- a/crates/oxc_transformer/src/options.rs +++ b/crates/oxc_transformer/src/options.rs @@ -20,6 +20,7 @@ pub struct TransformOptions { // es2016 pub exponentiation_operator: bool, // es2015 + pub function_name: bool, pub shorthand_properties: bool, pub sticky_regex: bool, pub template_literals: bool, diff --git a/justfile b/justfile index b5f011055..1b47420c8 100755 --- a/justfile +++ b/justfile @@ -51,6 +51,10 @@ check: test: cargo test +test-transform: + cargo run -p oxc_transform_conformance + cargo run -p oxc_transform_conformance -- --exec + # Lint the whole project lint: cargo lint -- --deny warnings diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index ca0e3cb1d..7ba1c7b6a 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -94,6 +94,7 @@ pub trait TestCase { .map(get_options::), assumptions: options.assumptions, class_static_block: options.get_plugin("transform-class-static-block").is_some(), + function_name: options.get_plugin("transform-function-name").is_some(), logical_assignment_operators: options .get_plugin("transform-logical-assignment-operators") .is_some(),