Skip to main content

quasiquodo_ts_core/
lib.rs

1mod context;
2pub mod input;
3mod lexer;
4mod lift;
5
6use proc_macro2::Span;
7use quote::ToTokens;
8use swc_common::comments::SingleThreadedComments;
9use swc_common::sync::Lrc;
10use swc_common::{FileName, SourceMap};
11use swc_ecma_parser::{Lexer, Parser, StringInput, Syntax, TsSyntax, error::Error as ParserError};
12use syn::parse_quote;
13
14use self::{
15    context::context,
16    input::{MacroInput, OutputKind},
17    lift::{CodeFragment, Lift},
18};
19
20#[cfg(test)]
21mod tests;
22
23/// Expands a `ts_quote!` invocation. Called by the proc-macro shim
24/// with the raw token stream.
25pub fn expand(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
26    match expand_inner(input) {
27        Ok(tokens) => tokens,
28        Err(err) => err.to_compile_error(),
29    }
30}
31
32fn expand_inner(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
33    let input: MacroInput = syn::parse2(input)?;
34
35    // Preprocess: replace `#{var}` placeholders with
36    // type-appropriate stand-ins.
37    let (preprocessed, stand_ins) = lexer::preprocess(input.source.value(), &input.variables)
38        .map_err(|err| syn::Error::new(input.source.span(), err))?;
39
40    let (parsed, comments) = parse_source(preprocessed, &input.output_kind)
41        .map_err(|err| syn::Error::new(input.source.span(), err))?;
42
43    let (stmts, context) = context(&input, stand_ins, comments)?;
44    let expr = match parsed.lift(&context)? {
45        CodeFragment::Single(expr) => expr,
46        CodeFragment::Splice(_) => {
47            return Err(syn::Error::new(
48                Span::call_site(),
49                "`Vec<T>` or `Option<T>` variable used in non-iterable position",
50            ));
51        }
52    };
53
54    let block: syn::Expr = parse_quote!({
55        #(#stmts)*
56        #expr
57    });
58
59    Ok(block.to_token_stream())
60}
61
62fn parse_source(
63    source: String,
64    kind: &OutputKind,
65) -> Result<(Box<dyn Lift>, SingleThreadedComments), ParseSourceError> {
66    use swc_ecma_ast::*;
67
68    // Wrap source if needed.
69    let input = match kind {
70        OutputKind::TsType => format!("type T = {source};"),
71        OutputKind::ClassMember => format!("class C {{ {source} }}"),
72        OutputKind::TsTypeElement => format!("interface I {{ {source} }}"),
73        OutputKind::ParamOrTsParamProp => {
74            format!("class C {{ constructor({source}) {{}} }}")
75        }
76        OutputKind::Param => format!("function f({source}) {{}}"),
77        OutputKind::ImportSpecifier => format!(r#"import {{{source} }} from "";"#),
78        OutputKind::ExportSpecifier => format!(r#"export {{{source} }} from "";"#),
79        OutputKind::Ident
80        | OutputKind::Expr
81        | OutputKind::Stmt
82        | OutputKind::ModuleItem
83        | OutputKind::Decl => source,
84    };
85
86    let source_map = Lrc::new(SourceMap::default());
87    let source_file = source_map.new_source_file(FileName::Anon.into(), input);
88    let comments = SingleThreadedComments::default();
89    let lexer = Lexer::new(
90        Syntax::Typescript(TsSyntax::default()),
91        EsVersion::latest(),
92        StringInput::from(&*source_file),
93        Some(&comments),
94    );
95    let mut parser = Parser::new_from(lexer);
96
97    let parsed: Box<dyn Lift> = match kind {
98        OutputKind::TsType => match parser.parse_module_item()? {
99            ModuleItem::Stmt(Stmt::Decl(Decl::TsTypeAlias(alias))) => Box::new(*alias.type_ann),
100            _ => Err(Expected::TypeAlias)?,
101        },
102        OutputKind::ClassMember => match parser.parse_module_item()? {
103            ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => {
104                let mut body = class_decl.class.body;
105                if body.len() != 1 {
106                    Err(Expected::ClassMember)?;
107                }
108                Box::new(body.swap_remove(0))
109            }
110            _ => Err(Expected::Class)?,
111        },
112        OutputKind::TsTypeElement => match parser.parse_module_item()? {
113            ModuleItem::Stmt(Stmt::Decl(Decl::TsInterface(iface))) => {
114                let mut body = iface.body.body;
115                if body.len() != 1 {
116                    Err(Expected::TypeElement)?;
117                }
118                Box::new(body.swap_remove(0))
119            }
120            _ => Err(Expected::Interface)?,
121        },
122        OutputKind::ParamOrTsParamProp => match parser.parse_module_item()? {
123            ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => {
124                let mut body = class_decl.class.body;
125                let Some(ClassMember::Constructor(mut ctor)) = body.pop() else {
126                    Err(Expected::Constructor)?
127                };
128                if ctor.params.len() != 1 {
129                    Err(Expected::ConstructorParam)?;
130                }
131                Box::new(ctor.params.swap_remove(0))
132            }
133            _ => Err(Expected::Class)?,
134        },
135        OutputKind::Param => match parser.parse_module_item()? {
136            ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => {
137                let mut params = fn_decl.function.params;
138                if params.len() != 1 {
139                    Err(Expected::Param)?;
140                }
141                Box::new(params.swap_remove(0))
142            }
143            _ => Err(Expected::Function)?,
144        },
145        OutputKind::ImportSpecifier => match parser.parse_module_item()? {
146            ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
147                let mut specifiers = import.specifiers;
148                if specifiers.len() != 1 {
149                    Err(Expected::ImportSpecifier)?;
150                }
151                Box::new(specifiers.swap_remove(0))
152            }
153            _ => Err(Expected::Import)?,
154        },
155        OutputKind::ExportSpecifier => match parser.parse_module_item()? {
156            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
157                let mut specifiers = export.specifiers;
158                if specifiers.len() != 1 {
159                    Err(Expected::ExportSpecifier)?;
160                }
161                Box::new(specifiers.swap_remove(0))
162            }
163            _ => Err(Expected::NamedExport)?,
164        },
165        OutputKind::Ident => match *parser.parse_expr()? {
166            Expr::Ident(ident) => Box::new(ident),
167            _ => Err(Expected::Identifier)?,
168        },
169        OutputKind::Expr => Box::new(*parser.parse_expr()?),
170        OutputKind::Stmt => {
171            // `parse_stmt_list_item` includes declarations
172            // (`const`, `let`, `class`, etc.), unlike `parse_stmt`.
173            Box::new(parser.parse_stmt_list_item()?)
174        }
175        OutputKind::ModuleItem => Box::new(parser.parse_module_item()?),
176        OutputKind::Decl => match parser.parse_module_item()? {
177            ModuleItem::Stmt(Stmt::Decl(decl)) => Box::new(decl),
178            _ => Err(Expected::Declaration)?,
179        },
180    };
181
182    Ok((parsed, comments))
183}
184
185#[derive(Debug, thiserror::Error)]
186enum ParseSourceError {
187    #[error("failed to parse TypeScript: {0:?}")]
188    Parser(ParserError),
189    #[error(transparent)]
190    Expected(#[from] Expected),
191}
192
193impl From<ParserError> for ParseSourceError {
194    fn from(err: ParserError) -> Self {
195        Self::Parser(err)
196    }
197}
198
199#[derive(Debug, thiserror::Error)]
200enum Expected {
201    #[error("expected a class declaration")]
202    Class,
203    #[error("expected exactly one class member")]
204    ClassMember,
205    #[error("expected exactly one type element")]
206    TypeElement,
207    #[error("expected a function declaration")]
208    Function,
209    #[error("expected exactly one param")]
210    Param,
211    #[error("expected a constructor")]
212    Constructor,
213    #[error("expected exactly one constructor param")]
214    ConstructorParam,
215    #[error("expected an import declaration")]
216    Import,
217    #[error("expected exactly one import specifier")]
218    ImportSpecifier,
219    #[error("expected a named export declaration")]
220    NamedExport,
221    #[error("expected exactly one export specifier")]
222    ExportSpecifier,
223    #[error("expected a type alias declaration")]
224    TypeAlias,
225    #[error("expected a declaration")]
226    Declaration,
227    #[error("expected an identifier")]
228    Identifier,
229    #[error("expected an interface declaration")]
230    Interface,
231}