feat(transform): support TemplateLiteral of babel/plugin-transform-template-literals (#1132)

Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
Wenzhe Wang 2023-11-03 22:19:44 +08:00 committed by GitHub
parent 278a1d6fee
commit f71cb9f1da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 2 deletions

View file

@ -1,3 +1,5 @@
mod shorthand_properties;
mod template_literals;
pub use shorthand_properties::ShorthandProperties;
pub use template_literals::TemplateLiterals;

View file

@ -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:
/// * <https://babel.dev/docs/babel-plugin-transform-template-literals>
/// * <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-template-literals>
pub struct TemplateLiterals<'a> {
ast: Rc<AstBuilder<'a>>,
}
impl<'a> TemplateLiterals<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(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<Expression<'a>>,
ast: &Rc<AstBuilder<'a>>,
) -> Option<Expression<'a>> {
// `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
}

View file

@ -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<ExponentiationOperator<'a>>,
// es2015
es2015_shorthand_properties: Option<ShorthandProperties<'a>>,
es2015_template_literals: Option<TemplateLiterals<'a>>,
}
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);
}

View file

@ -22,6 +22,7 @@ pub struct TransformOptions {
// es2015
pub shorthand_properties: bool,
pub sticky_regex: bool,
pub template_literals: bool,
}
/// See <https://www.typescriptlang.org/tsconfig#target>

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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(),
}
}