From f71cb9f1da257510ab454ea87b379b685da3fd68 Mon Sep 17 00:00:00 2001 From: Wenzhe Wang Date: Fri, 3 Nov 2023 22:19:44 +0800 Subject: [PATCH] feat(transform): support TemplateLiteral of babel/plugin-transform-template-literals (#1132) Co-authored-by: Boshen --- crates/oxc_transformer/src/es2015/mod.rs | 2 + .../src/es2015/template_literals.rs | 118 ++++++++++++++++++ crates/oxc_transformer/src/lib.rs | 4 + crates/oxc_transformer/src/options.rs | 1 + tasks/transform_conformance/babel.snap.md | 24 +++- .../transform_conformance/babel_exec.snap.md | 3 +- tasks/transform_conformance/src/lib.rs | 1 + tasks/transform_conformance/src/test_case.rs | 1 + 8 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 crates/oxc_transformer/src/es2015/template_literals.rs diff --git a/crates/oxc_transformer/src/es2015/mod.rs b/crates/oxc_transformer/src/es2015/mod.rs index 7ff2ccadb..9456cb146 100644 --- a/crates/oxc_transformer/src/es2015/mod.rs +++ b/crates/oxc_transformer/src/es2015/mod.rs @@ -1,3 +1,5 @@ mod shorthand_properties; +mod template_literals; pub use shorthand_properties::ShorthandProperties; +pub use template_literals::TemplateLiterals; diff --git a/crates/oxc_transformer/src/es2015/template_literals.rs b/crates/oxc_transformer/src/es2015/template_literals.rs new file mode 100644 index 000000000..fb7499999 --- /dev/null +++ b/crates/oxc_transformer/src/es2015/template_literals.rs @@ -0,0 +1,118 @@ +use oxc_allocator::Vec; +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::{Atom, Span, SPAN}; +use std::{mem, rc::Rc}; + +use crate::{TransformOptions, TransformTarget}; + +/// ES2015: Template Literals +/// +/// References: +/// * +/// * +pub struct TemplateLiterals<'a> { + ast: Rc>, +} + +impl<'a> TemplateLiterals<'a> { + pub fn new(ast: Rc>, options: &TransformOptions) -> Option { + (options.target < TransformTarget::ES2015 || options.template_literals) + .then(|| Self { ast }) + } + + pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) { + #[allow(clippy::single_match)] + match expr { + Expression::TemplateLiteral(template_literal) => { + let quasis = mem::replace(&mut template_literal.quasis, self.ast.new_vec()); + let mut nodes = self.ast.new_vec_with_capacity(quasis.len()); + let mut expr_iter = template_literal.expressions.iter_mut(); + for quasi in quasis { + if let Some(cooked) = &quasi.value.cooked { + if cooked.as_str() != "" { + let string_literal = StringLiteral::new(SPAN, cooked.clone()); + let string_literal = self.ast.literal_string_expression(string_literal); + nodes.push(string_literal); + } + } + + if let Some(expr) = expr_iter.next() { + let expr = self.ast.move_expression(expr); + if is_non_empty_string_literal(&expr) { + nodes.push(expr); + } + } + } + + // make sure the first node is a string + if !matches!(nodes.get(0), Some(Expression::StringLiteral(_))) { + let literal = StringLiteral::new(SPAN, Atom::from("")); + let string_literal = self.ast.literal_string_expression(literal); + nodes.insert(0, string_literal); + } + + if let Some(call_expr) = build_concat_call_expr(nodes, &Rc::clone(&self.ast)) { + *expr = call_expr; + } + } + // TODO: Expression::TaggedTemplateExpression + _ => {} + } + } +} + +/// This function groups the objects into multiple calls to `.concat()` in +/// order to preserve execution order of the primitive conversion +/// +/// ```javascript +/// "".concat(obj.foo, "foo", obj2.foo, "foo2") +/// ``` +/// The above code would evaluate both member expressions _first_ then, `concat` will +/// convert each one to a primitive, whereas +/// +/// ```javascript +/// "".concat(obj.foo, "foo").concat(obj2.foo, "foo2") +/// ``` +/// would evaluate the member, then convert it to a primitive, then evaluate +/// the second member and convert that one, which reflects the spec behavior +/// of template literals. +fn build_concat_call_expr<'a>( + nodes: Vec>, + ast: &Rc>, +) -> Option> { + // `1${"2"}${"3"}${a}${b}${"4"}${"5"}${c}` -> 1".concat("2", "3", a).concat(b, "4", "5").concat(c) + let mut avail = false; + nodes.into_iter().reduce(|mut left, right| { + let mut can_be_inserted = matches!(right, Expression::StringLiteral(_)); + + // for spec compatibility, we shouldn't keep two or more non-string node in one concat call. + // but we want group multiple node in one concat call as much as possible + // only the first encounter of non-string node can be inserted directly in the previous concat call + // other concat call will contains non-string node already. + if !can_be_inserted && avail { + can_be_inserted = true; + avail = false; + } + if can_be_inserted { + if let Expression::CallExpression(call_expr) = &mut left { + let argument = Argument::Expression(right); + call_expr.arguments.push(argument); + return left; + } + } + + let property = IdentifierName::new(Span::default(), "concat".into()); + let member_expr = ast.static_member_expression(Span::default(), left, property, false); + let arguments = ast.new_vec_single(Argument::Expression(right)); + let call_expr = ast.call_expression(Span::default(), member_expr, arguments, false, None); + call_expr + }) +} + +fn is_non_empty_string_literal(expr: &Expression) -> bool { + if let Expression::StringLiteral(string_literal) = expr { + return string_literal.value.as_str() != ""; + } + + true +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 238062086..03b63d909 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -24,6 +24,7 @@ mod utils; use std::{cell::RefCell, rc::Rc}; +use es2015::TemplateLiterals; use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ast::*, AstBuilder, VisitMut}; use oxc_semantic::Semantic; @@ -59,6 +60,7 @@ pub struct Transformer<'a> { es2016_exponentiation_operator: Option>, // es2015 es2015_shorthand_properties: Option>, + es2015_template_literals: Option>, } impl<'a> Transformer<'a> { @@ -85,6 +87,7 @@ impl<'a> Transformer<'a> { es2019_optional_catch_binding: OptionalCatchBinding::new(Rc::clone(&ast), &options), es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), ctx.clone(), &options), es2015_shorthand_properties: ShorthandProperties::new(Rc::clone(&ast), &options), + es2015_template_literals: TemplateLiterals::new(Rc::clone(&ast), &options), } } @@ -123,6 +126,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { self.es2021_logical_assignment_operators.as_mut().map(|t| t.transform_expression(expr)); self.es2020_nullish_coalescing_operators.as_mut().map(|t| t.transform_expression(expr)); self.es2016_exponentiation_operator.as_mut().map(|t| t.transform_expression(expr)); + self.es2015_template_literals.as_mut().map(|t| t.transform_expression(expr)); self.visit_expression_match(expr); } diff --git a/crates/oxc_transformer/src/options.rs b/crates/oxc_transformer/src/options.rs index 510b47ea7..9fc19a8cf 100644 --- a/crates/oxc_transformer/src/options.rs +++ b/crates/oxc_transformer/src/options.rs @@ -22,6 +22,7 @@ pub struct TransformOptions { // es2015 pub shorthand_properties: bool, pub sticky_regex: bool, + pub template_literals: bool, } /// See diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index b30270f31..eebfd1e66 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 242/1080 +Passed: 255/1113 # All Passed: * babel-plugin-transform-numeric-separator @@ -704,6 +704,28 @@ Passed: 242/1080 * unicode-regex/negated-set/input.js * unicode-regex/slash/input.js +# babel-plugin-transform-template-literals (13/33) +* assumption-ignoreToPrimitiveHint/escape-quotes/input.js +* assumption-ignoreToPrimitiveHint/expression-first/input.js +* assumption-ignoreToPrimitiveHint/functions/input.js +* assumption-ignoreToPrimitiveHint/literals/input.js +* assumption-ignoreToPrimitiveHint/multiple/input.js +* assumption-ignoreToPrimitiveHint/only/input.js +* assumption-ignoreToPrimitiveHint/single/input.js +* assumption-ignoreToPrimitiveHint/statement/input.js +* assumption-ignoreToPrimitiveHint/tag/input.js +* assumption-mutableTemplateObject/tag/input.js +* assumption-mutableTemplateObject/template-revision/input.js +* default/cache-revision/input.js +* default/literals/input.js +* default/simple-tag/input.js +* default/tag/input.js +* default/tag-with-unicode-escapes/input.js +* default/tag-with-unicode-escapes-babel-7/input.js +* default/template-revision/input.js +* loose/ignoreToPrimitiveHint/input.js +* loose/mutableTemplateObject/input.js + # babel-plugin-transform-typescript (84/181) * class/abstract-class-decorated/input.ts * class/abstract-class-decorated-method/input.ts diff --git a/tasks/transform_conformance/babel_exec.snap.md b/tasks/transform_conformance/babel_exec.snap.md index f29fe5c4f..252363216 100644 --- a/tasks/transform_conformance/babel_exec.snap.md +++ b/tasks/transform_conformance/babel_exec.snap.md @@ -1,4 +1,4 @@ -Passed: 349/408 +Passed: 356/415 # All Passed: * babel-plugin-transform-class-static-block @@ -7,6 +7,7 @@ Passed: 349/408 * babel-plugin-transform-json-strings * babel-plugin-transform-async-to-generator * babel-plugin-transform-exponentiation-operator +* babel-plugin-transform-template-literals # babel-plugin-transform-class-properties (132/143) diff --git a/tasks/transform_conformance/src/lib.rs b/tasks/transform_conformance/src/lib.rs index a45b0b2c6..9f639f3bf 100644 --- a/tasks/transform_conformance/src/lib.rs +++ b/tasks/transform_conformance/src/lib.rs @@ -77,6 +77,7 @@ const CASES: &[&str] = &[ "babel-plugin-transform-shorthand-properties", "babel-plugin-transform-sticky-regex", "babel-plugin-transform-unicode-regex", + "babel-plugin-transform-template-literals", // TypeScript "babel-plugin-transform-typescript", // React diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index 69a88332b..908bdba9b 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -106,6 +106,7 @@ pub trait TestCase { .is_some(), shorthand_properties: options.get_plugin("transform-shorthand-properties").is_some(), sticky_regex: options.get_plugin("transform-sticky-regex").is_some(), + template_literals: options.get_plugin("transform-template-literals").is_some(), } }