From 19102275904e757af29594b2aa90689afd7dcb9c Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Fri, 8 Nov 2024 08:18:44 +0000 Subject: [PATCH] feat(transformer/async-to-generator): support inferring the function name from the ObjectPropertyValue's key (#7201) Support for inferring function name from ObjectPropertyValue's key For example: ```js ({ foo: async function() {} }) ``` After this, we will able to infer `foo` for the object method --- .../src/es2017/async_to_generator.rs | 88 +++++++++++++++---- .../snapshots/semantic_typescript.snap | 12 +-- .../snapshots/oxc.snap.md | 2 +- .../object/property-with-function/input.js | 23 +++++ .../object/property-with-function/output.js | 55 ++++++++++++ 5 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/input.js create mode 100755 tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/output.js diff --git a/crates/oxc_transformer/src/es2017/async_to_generator.rs b/crates/oxc_transformer/src/es2017/async_to_generator.rs index 88fc5f25c..3b53de1b8 100644 --- a/crates/oxc_transformer/src/es2017/async_to_generator.rs +++ b/crates/oxc_transformer/src/es2017/async_to_generator.rs @@ -53,10 +53,14 @@ use std::mem; -use oxc_allocator::Box as ArenaBox; -use oxc_ast::{ast::*, Visit, NONE}; +use oxc_allocator::{Box as ArenaBox, String as ArenaString}; +use oxc_ast::{ast::*, AstBuilder, Visit, NONE}; use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags}; use oxc_span::{Atom, GetSpan, SPAN}; +use oxc_syntax::{ + identifier::{is_identifier_name, is_identifier_part, is_identifier_start}, + keyword::is_reserved_keyword, +}; use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx}; use crate::{common::helper_loader::Helper, TransformCtx}; @@ -287,9 +291,7 @@ impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> { let params = Self::create_placeholder_params(¶ms, scope_id, ctx); let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); - let id = id.or_else(|| { - Self::infer_function_id_from_variable_declarator(wrapper_scope_id, ctx) - }); + let id = id.or_else(|| Self::infer_function_id_from_parent_node(wrapper_scope_id, ctx)); Self::create_function(id, params, body, scope_id, ctx) }; @@ -425,7 +427,7 @@ impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> { let params = ctx.alloc(ctx.ast.move_formal_parameters(&mut arrow.params)); let generator_function_id = arrow.scope_id(); ctx.scopes_mut().get_flags_mut(generator_function_id).remove(ScopeFlags::Arrow); - let function_name = Self::get_function_name_from_parent_variable_declarator(ctx); + let function_name = Self::infer_function_name_from_parent_node(ctx); if function_name.is_none() && !Self::is_function_length_affected(¶ms) { return self.create_async_to_generator_call( @@ -479,25 +481,79 @@ impl<'a, 'ctx> AsyncGeneratorExecutor<'a, 'ctx> { } } - /// Infers the function id from [`Ancestor::VariableDeclaratorInit`]. - fn infer_function_id_from_variable_declarator( + /// Infers the function id from [`TraverseCtx::parent`]. + fn infer_function_id_from_parent_node( scope_id: ScopeId, ctx: &mut TraverseCtx<'a>, ) -> Option> { - let name = Self::get_function_name_from_parent_variable_declarator(ctx)?; + let name = Self::infer_function_name_from_parent_node(ctx)?; Some( ctx.generate_binding(name, scope_id, SymbolFlags::FunctionScopedVariable) .create_binding_identifier(ctx), ) } - fn get_function_name_from_parent_variable_declarator( - ctx: &mut TraverseCtx<'a>, - ) -> Option> { - let Ancestor::VariableDeclaratorInit(declarator) = ctx.parent() else { - return None; - }; - declarator.id().get_binding_identifier().map(|id| id.name.clone()) + /// Infers the function name from the [`TraverseCtx::parent`]. + fn infer_function_name_from_parent_node(ctx: &mut TraverseCtx<'a>) -> Option> { + match ctx.parent() { + // infer `foo` from `const foo = async function() {}` + Ancestor::VariableDeclaratorInit(declarator) => { + declarator.id().get_binding_identifier().map(|id| id.name.clone()) + } + // infer `foo` from `({ foo: async function() {} })` + Ancestor::ObjectPropertyValue(property) if !*property.method() => { + property.key().static_name().map(|key| Self::normalize_function_name(&key, ctx.ast)) + } + _ => None, + } + } + + /// Normalizes the function name. + /// + /// Examples: + /// + /// // Valid + /// * `foo` -> `foo` + /// // Contains space + /// * `foo bar` -> `foo_bar` + /// // Reserved keyword + /// * `this` -> `_this` + /// * `arguments` -> `_arguments` + fn normalize_function_name(input: &str, ast: AstBuilder<'a>) -> Atom<'a> { + if !is_reserved_keyword(input) && is_identifier_name(input) { + return ast.atom(input); + } + + let mut name = ArenaString::with_capacity_in(input.len() + 1, ast.allocator); + let mut capitalize_next = false; + + let mut chars = input.chars(); + if let Some(first) = chars.next() { + if is_identifier_start(first) { + name.push(first); + } + } + + for c in chars { + if c == ' ' { + name.push('_'); + } else if !is_identifier_part(c) { + capitalize_next = true; + } else if capitalize_next { + name.push(c.to_ascii_uppercase()); + capitalize_next = false; + } else { + name.push(c); + } + } + + if name.is_empty() { + return ast.atom("_"); + } else if is_reserved_keyword(name.as_str()) { + name.insert(0, '_'); + } + + ast.atom(name.into_bump_str()) } /// Creates a [`Function`] with the specified params, body and scope_id. diff --git a/tasks/coverage/snapshots/semantic_typescript.snap b/tasks/coverage/snapshots/semantic_typescript.snap index 11a712af2..075bdf2c9 100644 --- a/tasks/coverage/snapshots/semantic_typescript.snap +++ b/tasks/coverage/snapshots/semantic_typescript.snap @@ -825,7 +825,7 @@ Bindings mismatch: after transform: ScopeId(22): ["T", "value"] rebuilt : ScopeId(29): ["value"] Symbol flags mismatch for "_asyncToGenerator": -after transform: SymbolId(26): SymbolFlags(Import) +after transform: SymbolId(27): SymbolFlags(Import) rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable) Unresolved references mismatch: after transform: ["Promise", "arguments", "require"] @@ -5338,17 +5338,17 @@ semantic error: Bindings mismatch: after transform: ScopeId(0): ["LoadCallback", "_asyncToGenerator", "cb1", "cb2", "cb3", "fn1"] rebuilt : ScopeId(0): ["_asyncToGenerator", "cb1", "cb2", "cb3", "fn1"] Scope children mismatch: -after transform: ScopeId(0): [ScopeId(1), ScopeId(2), ScopeId(3), ScopeId(5), ScopeId(7), ScopeId(8), ScopeId(12), ScopeId(14), ScopeId(15), ScopeId(17)] -rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2), ScopeId(4), ScopeId(6), ScopeId(10), ScopeId(13)] +after transform: ScopeId(0): [ScopeId(1), ScopeId(2), ScopeId(5), ScopeId(7), ScopeId(8), ScopeId(12), ScopeId(14), ScopeId(16), ScopeId(17), ScopeId(19)] +rebuilt : ScopeId(0): [ScopeId(1), ScopeId(4), ScopeId(6), ScopeId(8), ScopeId(12), ScopeId(15)] Symbol flags mismatch for "_asyncToGenerator": -after transform: SymbolId(10): SymbolFlags(Import) +after transform: SymbolId(12): SymbolFlags(Import) rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable) Unresolved references mismatch: after transform: ["Promise", "Record", "StateMachine", "arguments", "createMachine", "load", "require"] rebuilt : ["Promise", "arguments", "createMachine", "load", "require"] Unresolved reference IDs mismatch for "Promise": after transform: [ReferenceId(2), ReferenceId(7), ReferenceId(8), ReferenceId(9), ReferenceId(11), ReferenceId(12), ReferenceId(13)] -rebuilt : [ReferenceId(3), ReferenceId(5), ReferenceId(7)] +rebuilt : [ReferenceId(3), ReferenceId(7), ReferenceId(9)] tasks/coverage/typescript/tests/cases/compiler/contextuallyTypeGeneratorReturnTypeFromUnion.ts semantic error: Bindings mismatch: @@ -55663,7 +55663,7 @@ semantic error: Scope children mismatch: after transform: ScopeId(0): [ScopeId(1), ScopeId(2)] rebuilt : ScopeId(0): [ScopeId(1)] Symbol flags mismatch for "_asyncToGenerator": -after transform: SymbolId(10): SymbolFlags(Import) +after transform: SymbolId(11): SymbolFlags(Import) rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable) tasks/coverage/typescript/tests/cases/conformance/types/rest/objectRestParameter.ts diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 2b3243b88..67c76d59b 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: d20b314c -Passed: 79/88 +Passed: 80/89 # All Passed: * babel-plugin-transform-class-static-block diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/input.js new file mode 100644 index 000000000..5bf71d1f0 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/input.js @@ -0,0 +1,23 @@ +const Normal = { + foo: async () => { + console.log(log) + } +} + +const StringLiteralKey = { + ['bar']: async () => { + } +} + +const EmptyStringLiteralKey = { + ['']: async () => { + console.log(this) + } +} + +const InvalidStringLiteralKey = { + ['#']: async () => {}, + ['this']: async () => {}, + ['#default']: async () => {}, + ['O X C']: async () => {} +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/output.js new file mode 100755 index 000000000..96683c724 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/object/property-with-function/output.js @@ -0,0 +1,55 @@ +var _this = this; +const Normal = { + foo: function () { + var _ref = babelHelpers.asyncToGenerator(function* () { + console.log(log); + }); + return function foo() { + return _ref.apply(this, arguments); + }; + }() +}; +const StringLiteralKey = { + ['bar']: function () { + var _ref2 = babelHelpers.asyncToGenerator(function* () {}); + return function bar() { + return _ref2.apply(this, arguments); + }; + }() +}; +const EmptyStringLiteralKey = { + ['']: function () { + var _ref3 = babelHelpers.asyncToGenerator(function* () { + console.log(_this); + }); + return function _() { + return _ref3.apply(this, arguments); + }; + }() +}; +const InvalidStringLiteralKey = { + ['#']: function () { + var _ref4 = babelHelpers.asyncToGenerator(function* () {}); + return function _() { + return _ref4.apply(this, arguments); + }; + }(), + ['this']: function () { + var _ref5 = babelHelpers.asyncToGenerator(function* () {}); + return function _this() { + return _ref5.apply(this, arguments); + }; + }(), + ['#default']: function () { + var _ref6 = babelHelpers.asyncToGenerator(function* () {}); + return function _default() { + return _ref6.apply(this, arguments); + }; + }(), + ['O X C']: function () { + var _ref7 = babelHelpers.asyncToGenerator(function* () {}); + return function O_X_C() { + return _ref7.apply(this, arguments); + }; + }() +}; \ No newline at end of file