mirror of
https://github.com/danbulant/oxc
synced 2026-05-21 13:18:59 +00:00
feat(transform): support TemplateLiteral of babel/plugin-transform-template-literals (#1132)
Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
parent
278a1d6fee
commit
f71cb9f1da
8 changed files with 152 additions and 2 deletions
|
|
@ -1,3 +1,5 @@
|
|||
mod shorthand_properties;
|
||||
mod template_literals;
|
||||
|
||||
pub use shorthand_properties::ShorthandProperties;
|
||||
pub use template_literals::TemplateLiterals;
|
||||
|
|
|
|||
118
crates/oxc_transformer/src/es2015/template_literals.rs
Normal file
118
crates/oxc_transformer/src/es2015/template_literals.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue