Repository: swc-project/swc (rust)

Issue:
New crate `swc_ecma_template`
### Describe the feature

new crate `swc_ecma_template` that has a same feature as [`@babel/template`](https://babeljs.io/docs/en/babel-template).

```rs
let ast = template!("const %%name%% = 3", "foo");
```

equals to

```rs
let ast = Decl::Var(VarDecl {
    span: DUMMY_SP,
    kind: VarDeclKind::Const,
    declare: false,
    decls: vec![VarDeclarator {
        span: DUMMY_SP,
        name: Pat::Ident(BindingIdent {
            id: Ident {
                span: DUMMY_SP,
                sym: "foo".into(),
                optional: false,
            },
            type_ann: None,
        }),
        init: Some(Box::new(Expr::Lit(Lit::Num(Number {
            span: DUMMY_SP,
            value: 3.0,
        })))),
        definite: false,
    }],
  });
}
```

### Babel plugin or link to the feature description

https://babeljs.io/docs/en/babel-template

### Additional context

I'm not familiar with Rust, so I'm not sure that such macros are possible. However if it is possible, it will help plugin and transformer developers.

Pull request description:
feat(es/quote): Support variables
**Description:**

We support implicit cloning, but it's restricted only to support some cheap-to-clone types.

We can improve error messages for using non-clonable type multiple times **if** we use nightly features.


**Related issue (if exists):**
 - Closes #3146


Patch:
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 712502acb5..467f53c656 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -115,3 +115,7 @@ jobs:
         if: steps.bump.outcome == 'success' && steps.bump.conclusion == 'success'
         run: |
           cargo mono publish --no-verify
+          cargo mono publish --no-verify
+          cargo mono publish --no-verify
+          cargo mono publish --no-verify
+          cargo mono publish --no-verify
diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml
index a248208368..bb379fdc22 100644
--- a/.github/workflows/cargo.yml
+++ b/.github/workflows/cargo.yml
@@ -181,6 +181,10 @@ jobs:
             os: ubuntu-latest
           - crate: swc_ecma_preset_env
             os: windows-latest
+          - crate: swc_ecma_quote
+            os: ubuntu-latest
+          - crate: swc_ecma_quote_macros
+            os: ubuntu-latest
           - crate: swc_ecma_transforms
             os: ubuntu-latest
             check: |
@@ -266,6 +270,8 @@ jobs:
             os: ubuntu-latest
           - crate: swc_timer
             os: ubuntu-latest
+          - crate: swc_trace_macro
+            os: ubuntu-latest
           - crate: swc_visit
             os: ubuntu-latest
           - crate: swc_visit_macros
diff --git a/Cargo.lock b/Cargo.lock
index 323e180f47..d5852c597b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3134,6 +3134,8 @@ dependencies = [
  "swc_common",
  "swc_ecma_ast",
  "swc_ecma_quote_macros",
+ "swc_ecma_utils",
+ "testing",
 ]
 
 [[package]]
diff --git a/crates/swc_ecma_parser/src/parser/stmt.rs b/crates/swc_ecma_parser/src/parser/stmt.rs
index 0fe92cf9fc..7020f42f39 100644
--- a/crates/swc_ecma_parser/src/parser/stmt.rs
+++ b/crates/swc_ecma_parser/src/parser/stmt.rs
@@ -63,12 +63,14 @@ impl<'a, I: Tokens> Parser<I> {
         Ok(stmts.into_vec())
     }
 
+    /// Parse a statement but not a declaration.
     pub fn parse_stmt(&mut self, top_level: bool) -> PResult<Stmt> {
         trace_cur!(self, parse_stmt);
         self.parse_stmt_like(false, top_level)
     }
 
-    fn parse_stmt_list_item(&mut self, top_level: bool) -> PResult<Stmt> {
+    /// Parse a statement and maybe a declaration.
+    pub fn parse_stmt_list_item(&mut self, top_level: bool) -> PResult<Stmt> {
         trace_cur!(self, parse_stmt_list_item);
         self.parse_stmt_like(true, top_level)
     }
diff --git a/crates/swc_ecma_quote/Cargo.toml b/crates/swc_ecma_quote/Cargo.toml
index 2d13cf5324..f6caa0b106 100644
--- a/crates/swc_ecma_quote/Cargo.toml
+++ b/crates/swc_ecma_quote/Cargo.toml
@@ -15,3 +15,5 @@ swc_atoms = {version = "0.2.9", path = "../swc_atoms"}
 swc_common = {version = "0.17.9", path = "../swc_common"}
 swc_ecma_ast = {version = "0.68.3", path = "../swc_ecma_ast"}
 swc_ecma_quote_macros = {version = "0.1.0", path = "../swc_ecma_quote_macros"}
+swc_ecma_utils = {version = "0.69.0", path = "../swc_ecma_utils"}
+testing = {version = "0.18.0", path = "../testing"}
diff --git a/crates/swc_ecma_quote/src/clone.rs b/crates/swc_ecma_quote/src/clone.rs
new file mode 100644
index 0000000000..1820c93d7b
--- /dev/null
+++ b/crates/swc_ecma_quote/src/clone.rs
@@ -0,0 +1,35 @@
+//! Module for implicitly copyable types.
+
+use swc_atoms::JsWord;
+use swc_ecma_ast::*;
+
+use self::private::Sealed;
+
+/// Not a public API.
+///
+/// Implemented for types which will be implicitly cloned when used as a
+/// variable in [crate::quote] macro calls.
+pub trait ImplicitClone: Clone + Sealed {
+    fn clone_quote_var(&self) -> Self {
+        self.clone()
+    }
+}
+
+macro_rules! impl_for {
+    ($T:ty) => {
+        impl ImplicitClone for $T {}
+        impl Sealed for $T {}
+    };
+}
+
+impl_for!(Id);
+impl_for!(Ident);
+impl_for!(JsWord);
+
+impl_for!(Str);
+impl_for!(Number);
+impl_for!(Bool);
+
+mod private {
+    pub trait Sealed {}
+}
diff --git a/crates/swc_ecma_quote/src/lib.rs b/crates/swc_ecma_quote/src/lib.rs
index 84ad3b04a6..a3fb8eee3d 100644
--- a/crates/swc_ecma_quote/src/lib.rs
+++ b/crates/swc_ecma_quote/src/lib.rs
@@ -8,6 +8,11 @@ pub extern crate swc_ecma_ast;
 #[doc(hidden)]
 pub extern crate swc_ecma_quote_macros;
 
+#[doc(hidden)]
+pub use self::clone::ImplicitClone;
+
+mod clone;
+
 /// # Supported output types
 ///
 ///  - `Expr`
@@ -22,15 +27,20 @@ pub extern crate swc_ecma_quote_macros;
 ///
 /// # Variable substitution
 ///
-/// (**Not implemented**)
-///
 /// If an identifier starts with `$`, it is substituted with the value of the
 /// parameter passed.
 ///
 /// e.g.
 ///
-/// ```rust,ignore
-/// quote!("const $name = 4;" as Stmt, name = private_ident!("ref"))
+/// ```rust
+/// use swc_common::DUMMY_SP;
+/// use swc_ecma_ast::Ident;
+/// use swc_ecma_quote::quote;
+///
+/// // This will return ast for `const ref = 4;`
+/// let _stmt = quote!("const $name = 4;" as Stmt, name = Ident::new("ref".into(), DUMMY_SP));
+///
+/// // Tip: Use private_ident!("ref") for real identifiers.
 /// ```
 ///
 ///
@@ -38,13 +48,15 @@ pub extern crate swc_ecma_quote_macros;
 ///
 /// ## Quote a variable declaration
 ///
-/// (**Not implemented**)
-///
 /// ```rust
-/// use swc_ecma_ast::*;
+/// use swc_common::DUMMY_SP;
+/// use swc_ecma_ast::Ident;
 /// use swc_ecma_quote::quote;
 ///
-/// let stmt = quote!("const $name = 4;" as Stmt, name = private_ident!("ref"));
+/// // This will return ast for `const ref = 4;`
+/// let _stmt = quote!("const $name = 4;" as Stmt, name = Ident::new("ref".into(), DUMMY_SP));
+///
+/// // Tip: Use private_ident!("ref") for real identifiers.
 /// ```
 #[macro_export]
 macro_rules! quote {
@@ -58,11 +70,11 @@ macro_rules! quote {
 /// This is an alias for [quote], but without `as Box<Expr>`.
 #[macro_export]
 macro_rules! quote_expr {
-    ($($tt1:tt)*) => {{
-        $crate::quote!($($tt1)* as Box<Expr>)
+    ($src:tt) => {{
+        $crate::quote!($src as Box<Expr>)
     }};
 
-    ($($tt1:tt)*, $($tt2:tt)*) => {{
-        $crate::quote!($($tt1)* as Box<Expr>, $($tt2)*)
+    ($src:tt, $($tt2:tt)*) => {{
+        $crate::quote!($src as Box<Expr>, $($tt2)*)
     }};
 }
diff --git a/crates/swc_ecma_quote_macros/src/ast/id.rs b/crates/swc_ecma_quote_macros/src/ast/id.rs
index 21bb8931e3..8751dd660f 100644
--- a/crates/swc_ecma_quote_macros/src/ast/id.rs
+++ b/crates/swc_ecma_quote_macros/src/ast/id.rs
@@ -8,6 +8,12 @@ impl ToCode for swc_ecma_ast::Ident {
     fn to_code(&self, cx: &Ctx) -> syn::Expr {
         // TODO: Check for variables.
 
+        if let Some(var_name) = self.sym.strip_prefix('$') {
+            if let Some(var) = cx.vars.get(var_name) {
+                return var.get_expr();
+            }
+        }
+
         q!(
             Vars {
                 sym_value: self.sym.to_code(cx),
diff --git a/crates/swc_ecma_quote_macros/src/ast/lit.rs b/crates/swc_ecma_quote_macros/src/ast/lit.rs
index 8a00ae1108..c4544cbe94 100644
--- a/crates/swc_ecma_quote_macros/src/ast/lit.rs
+++ b/crates/swc_ecma_quote_macros/src/ast/lit.rs
@@ -42,7 +42,7 @@ impl ToCode for f64 {
     fn to_code(&self, _: &Ctx) -> syn::Expr {
         syn::Expr::Lit(ExprLit {
             attrs: Default::default(),
-            lit: syn::Lit::Float(LitFloat::new(&self.to_string(), Span::call_site())),
+            lit: syn::Lit::Float(LitFloat::new(&format!("{}f64", self), Span::call_site())),
         })
     }
 }
diff --git a/crates/swc_ecma_quote_macros/src/ctxt.rs b/crates/swc_ecma_quote_macros/src/ctxt.rs
index cc9d65b341..16fba44ff9 100644
--- a/crates/swc_ecma_quote_macros/src/ctxt.rs
+++ b/crates/swc_ecma_quote_macros/src/ctxt.rs
@@ -1,7 +1,12 @@
 #![allow(unused)]
 
+use std::cell::RefCell;
+
+use pmutil::q;
 use swc_common::collections::AHashMap;
-use swc_ecma_ast::Ident;
+use syn::{parse_quote, punctuated::Punctuated, ExprPath, ExprReference, Token};
+
+use crate::{ast::ToCode, input::QuoteVar};
 
 #[derive(Debug)]
 pub(crate) struct Ctx {
@@ -10,11 +15,93 @@ pub(crate) struct Ctx {
 
 #[derive(Debug)]
 pub struct VarData {
+    is_counting: bool,
+
     /// How many times this variable should be cloned. 0 for variables used only
     /// once.
-    clone: usize,
+    clone: RefCell<usize>,
 
-    ident: Ident,
+    ident: syn::Ident,
+}
+
+impl VarData {
+    pub fn get_expr(&self) -> syn::Expr {
+        if self.is_counting {
+            *self.clone.borrow_mut() += 1;
+            return self.expr_for_var_ref();
+        }
+
+        let use_clone = {
+            let mut b = self.clone.borrow_mut();
+            let val = *b;
+            if val > 0 {
+                *b -= 1;
+                val != 1
+            } else {
+                false
+            }
+        };
+
+        if use_clone {
+            let var_ref_expr = self.expr_for_var_ref();
+
+            parse_quote!(swc_ecma_quote::ImplicitClone::clone_quote_var(&#var_ref_expr))
+        } else {
+            self.expr_for_var_ref()
+        }
+    }
+
+    fn expr_for_var_ref(&self) -> syn::Expr {
+        syn::Expr::Path(ExprPath {
+            attrs: Default::default(),
+            qself: Default::default(),
+            path: self.ident.clone().into(),
+        })
+    }
 }
 
 pub type Vars = AHashMap<String, VarData>;
+
+pub(super) fn prepare_vars(
+    src: &dyn ToCode,
+    vars: Punctuated<QuoteVar, Token![,]>,
+) -> (Vec<syn::Stmt>, Vars) {
+    let mut stmts = vec![];
+    let mut init_map = Vars::default();
+
+    for var in vars {
+        let value = var.value;
+
+        let ident = var.name.clone();
+        let ident_str = ident.to_string();
+
+        let var_ident = syn::Ident::new(&format!("quote_var_{}", ident), ident.span());
+
+        let old = init_map.insert(
+            ident_str.clone(),
+            VarData {
+                is_counting: true,
+                clone: Default::default(),
+                ident: var_ident.clone(),
+            },
+        );
+
+        if let Some(old) = old {
+            panic!("Duplicate variable name: {}", ident_str);
+        }
+
+        stmts.push(parse_quote! {
+            let #var_ident = #value;
+        });
+    }
+
+    // Use `ToCode` to count how many times each variable is used.
+    let mut cx = Ctx { vars: init_map };
+
+    src.to_code(&cx);
+
+    // We are done
+    cx.vars.iter_mut().for_each(|(k, v)| v.is_counting = false);
+
+    (stmts, cx.vars)
+}
diff --git a/crates/swc_ecma_quote_macros/src/input.rs b/crates/swc_ecma_quote_macros/src/input.rs
index fc7a3a1e31..da05158874 100644
--- a/crates/swc_ecma_quote_macros/src/input.rs
+++ b/crates/swc_ecma_quote_macros/src/input.rs
@@ -13,7 +13,6 @@ pub(super) struct QuoteInput {
     pub vars: Option<(Token![,], Punctuated<QuoteVar, Token![,]>)>,
 }
 
-#[allow(unused)]
 pub(super) struct QuoteVar {
     pub name: syn::Ident,
     #[allow(unused)]
diff --git a/crates/swc_ecma_quote_macros/src/lib.rs b/crates/swc_ecma_quote_macros/src/lib.rs
index c1bb2852da..ed7d1c44ed 100644
--- a/crates/swc_ecma_quote_macros/src/lib.rs
+++ b/crates/swc_ecma_quote_macros/src/lib.rs
@@ -1,10 +1,16 @@
 extern crate proc_macro;
 
-use std::collections::HashMap;
+use std::iter::once;
 
 use quote::ToTokens;
+use syn::{Block, ExprBlock};
 
-use crate::{ast::ToCode, ctxt::Ctx, input::QuoteInput, ret_type::parse_input_type};
+use crate::{
+    ast::ToCode,
+    ctxt::{prepare_vars, Ctx},
+    input::QuoteInput,
+    ret_type::parse_input_type,
+};
 
 mod ast;
 mod builder;
@@ -27,14 +33,28 @@ pub fn internal_quote(input: proc_macro::TokenStream) -> proc_macro::TokenStream
         parse_input_type(&src.value(), &output_type).expect("failed to parse input type");
 
     let vars = vars.map(|v| v.1);
-    let map = HashMap::default();
 
-    if let Some(vars) = vars {
-        if !vars.is_empty() {
-            todo!("quote! macro does not support variables yet")
-        }
-    }
-
-    let cx = Ctx { vars: map };
-    ret_type.to_code(&cx).to_token_stream().into()
+    let (stmts, vars) = if let Some(vars) = vars {
+        prepare_vars(&ret_type, vars)
+    } else {
+        Default::default()
+    };
+
+    let cx = Ctx { vars };
+
+    let expr_for_ast_creation = ret_type.to_code(&cx);
+
+    syn::Expr::Block(ExprBlock {
+        attrs: Default::default(),
+        label: Default::default(),
+        block: Block {
+            brace_token: Default::default(),
+            stmts: stmts
+                .into_iter()
+                .chain(once(syn::Stmt::Expr(expr_for_ast_creation)))
+                .collect(),
+        },
+    })
+    .to_token_stream()
+    .into()
 }
diff --git a/crates/swc_ecma_quote_macros/src/ret_type.rs b/crates/swc_ecma_quote_macros/src/ret_type.rs
index 7afd82393e..3255f233f1 100644
--- a/crates/swc_ecma_quote_macros/src/ret_type.rs
+++ b/crates/swc_ecma_quote_macros/src/ret_type.rs
@@ -38,7 +38,7 @@ pub(crate) fn parse_input_type(input_str: &str, ty: &Type) -> Result<BoxWrapper,
             match &*ident.to_string() {
                 "Expr" => return parse(input_str, &mut |p| p.parse_expr().map(|v| *v)),
                 "Pat" => return parse(input_str, &mut |p| p.parse_pat()),
-                "Stmt" => return parse(input_str, &mut |p| p.parse_stmt(true)),
+                "Stmt" => return parse(input_str, &mut |p| p.parse_stmt_list_item(true)),
                 "ModuleItem" => return parse(input_str, &mut |p| p.parse_module_item()),
                 _ => {}
             }


Test patch:
diff --git a/crates/swc_ecma_quote/tests/simple.rs b/crates/swc_ecma_quote/tests/simple.rs
index 48565f5159..c3fb12db0c 100644
--- a/crates/swc_ecma_quote/tests/simple.rs
+++ b/crates/swc_ecma_quote/tests/simple.rs
@@ -1,6 +1,29 @@
-use swc_ecma_quote::quote_expr;
+use swc_common::DUMMY_SP;
+use swc_ecma_ast::Ident;
+use swc_ecma_quote::{quote, quote_expr};
+use swc_ecma_utils::private_ident;
 
 #[test]
 fn quote_expr_call_1() {
     let _expr = quote_expr!("call(arg1, typeof arg2, arg3)");
 }
+
+#[test]
+fn quote_expr_var_cloned() {
+    testing::run_test2(false, |_cm, _handler| {
+        let id = private_ident!("_ref");
+
+        let _expr = quote_expr!("call($my_id, typeof arg2, $my_id)", my_id = id);
+
+        Ok(())
+    })
+    .unwrap();
+}
+
+#[test]
+fn quote_example() {
+    let _stmt = quote!(
+        "const $name = 4;" as Stmt,
+        name = Ident::new("ref".into(), DUMMY_SP)
+    );
+}