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:
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)
+    );
+}