/* * Author: Ruben Fiszel * Copyright: Windmill Labs, Inc 2022 * This file and its contents are licensed under the AGPLv3 License. * Please see the included NOTICE for copyright information and * LICENSE-AGPL for a copy of the license. */ use crate::{ error, js_eval::eval_sync, parser::{Arg, MainArgSignature, ObjectProperty, Typ}, }; use serde_json::Value; use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned}; use swc_ecma_ast::{ ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit, ModuleDecl, ModuleItem, Number, ObjectLit, Pat, Str, TsArrayType, TsEntityName, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType, }; use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig}; pub fn parse_deno_signature(code: &str) -> error::Result { let cm: Lrc = Default::default(); let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into()); let lexer = Lexer::new( // We want to parse ecmascript Syntax::Typescript(TsConfig::default()), // EsVersion defaults to es5 Default::default(), StringInput::from(&*fm), None, ); let mut parser = Parser::new_from(lexer); let mut err_s = "".to_string(); for e in parser.take_errors() { err_s += &e.into_kind().msg().to_string(); } let ast = parser .parse_module() .map_err(|_| { error::Error::ExecutionErr(format!( "Error while parsing code, it is invalid typescript" )) })? .body; // println!("{ast:?}"); let params = ast.into_iter().find_map(|x| match x { ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, function, .. }), .. })) if &sym.to_string() == "main" => Some(function.params), _ => None, }); if let Some(params) = params { Ok(MainArgSignature { star_args: false, star_kwargs: false, args: params .into_iter() .map(|x| match x.pat { Pat::Ident(ident) => { let (name, typ, nullable) = binding_ident_to_arg(&ident); Ok(Arg { otyp: None, name, typ, default: None, has_default: ident.id.optional || nullable, }) } Pat::Assign(AssignPat { left, right, .. }) => { let (name, mut typ, _nullable) = left.as_ident().map(binding_ident_to_arg).ok_or_else(|| { error::Error::ExecutionErr(format!( "parameter syntax unsupported: `{}`", cm.span_to_snippet(left.span()) .unwrap_or_else(|_| cm.span_to_string(left.span())) )) })?; let span = match *right { Expr::Lit(Lit::Str(Str { span, .. })) => Some(span), Expr::Lit(Lit::Num(Number { span, .. })) => Some(span), Expr::Lit(Lit::BigInt(BigInt { span, .. })) => Some(span), Expr::Lit(Lit::Bool(Bool { span, .. })) => Some(span), Expr::Object(ObjectLit { span, .. }) => Some(span), Expr::Array(ArrayLit { span, .. }) => Some(span), _ => None, }; let expr = span .and_then(|x| cm.span_to_snippet(x).ok()) .map(|x| serde_json::from_str(&x).map_err(|_| x)); let default = match expr.clone() { Some(Ok(x)) => Some(x), Some(Err(x)) => eval_sync(&x).ok(), None => None, }; if typ == Typ::Unknown && default.is_some() { typ = json_to_typ(default.as_ref().unwrap()); } Ok(Arg { otyp: None, name, typ, default, has_default: true }) } _ => Err(error::Error::ExecutionErr(format!( "parameter syntax unsupported: `{}`", cm.span_to_snippet(x.span()) .unwrap_or_else(|_| cm.span_to_string(x.span())) ))), }) .collect::, error::Error>>()?, }) } else { Err(error::Error::ExecutionErr( "main function was not findable (expected to find 'export function main(...)'" .to_string(), )) } } fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) { let (typ, nullable) = type_ann .as_ref() .map(|x| tstype_to_typ(&*x.type_ann)) .unwrap_or((Typ::Unknown, false)); (id.sym.to_string(), typ, nullable) } fn json_to_typ(js: &Value) -> Typ { match js { Value::String(_) => Typ::Str(None), Value::Number(n) if n.is_i64() => Typ::Int, Value::Number(_) => Typ::Float, Value::Bool(_) => Typ::Bool, Value::Object(o) => Typ::Object( o.iter() .map(|(k, v)| ObjectProperty { key: k.to_string(), typ: Box::new(json_to_typ(v)) }) .collect(), ), Value::Array(a) => Typ::List(Box::new(a.first().map(json_to_typ).unwrap_or(Typ::Unknown))), _ => Typ::Unknown, } } fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) { //println!("{:?}", ts_type); match ts_type { TsType::TsKeywordType(t) => ( match t.kind { TsKeywordTypeKind::TsObjectKeyword => Typ::Object(vec![]), TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool, TsKeywordTypeKind::TsBigIntKeyword => Typ::Int, TsKeywordTypeKind::TsNumberKeyword => Typ::Float, TsKeywordTypeKind::TsStringKeyword => Typ::Str(None), _ => Typ::Unknown, }, false, ), TsType::TsTypeLit(TsTypeLit { members, .. }) => { let properties = members .into_iter() .filter_map(|x| match x { TsTypeElement::TsPropertySignature(TsPropertySignature { key, type_ann, .. }) => match (*key.to_owned(), type_ann) { (Expr::Ident(Ident { sym, .. }), type_ann) => Some(ObjectProperty { key: sym.to_string(), typ: type_ann .as_ref() .map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0)) .unwrap_or(Box::new(Typ::Unknown)), }), _ => None, }, _ => None, }) .collect(); (Typ::Object(properties), false) } // TODO: we can do better here and extract the inner type of array TsType::TsArrayType(TsArrayType { elem_type, .. }) => { (Typ::List(Box::new(tstype_to_typ(&**elem_type).0)), false) } TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) => { (Typ::Str(Some(vec![value.to_string()])), false) } TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => { (tstype_to_typ(type_ann).0, true) } TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType( TsUnionType { types, .. }, )) => { if let Some(p) = if types.len() != 2 { None } else { types.into_iter().position(|x| match **x { TsType::TsKeywordType(TsKeywordType { kind, .. }) => { kind == TsKeywordTypeKind::TsUndefinedKeyword || kind == TsKeywordTypeKind::TsNullKeyword } _ => false, }) } { let other_p = if p == 0 { 1 } else { 0 }; (tstype_to_typ(&types[other_p]).0, true) } else { let literals = types .into_iter() .map(|x| match &**x { TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) => Some(value.to_string()), _ => None, }) .collect::>(); if literals.iter().find(|x| x.is_none()).is_some() { (Typ::Unknown, false) } else { ( Typ::Str(Some(literals.into_iter().filter_map(|x| x).collect())), false, ) } } } TsType::TsTypeRef(TsTypeRef { type_name, type_params, .. }) => { let sym = match type_name { TsEntityName::Ident(Ident { sym, .. }) => sym, TsEntityName::TsQualifiedName(p) => &*p.right.sym, }; match sym.to_string().as_str() { "Resource" => ( Typ::Resource( type_params .as_ref() .and_then(|x| { x.params.get(0).and_then(|y| { y.as_ts_lit_type().and_then(|z| { z.lit.as_str().map(|a| a.to_owned().value.to_string()) }) }) }) .unwrap_or_else(|| "unknown".to_string()), ), false, ), "Base64" => (Typ::Bytes, false), "Email" => (Typ::Email, false), "Sql" => (Typ::Sql, false), _ => (Typ::Unknown, false), } } _ => (Typ::Unknown, false), } } #[cfg(test)] mod tests { use serde_json::json; // Note this useful idiom: importing names from outer (for mod tests) scope. use super::*; #[test] fn test_parse_deno_sig() -> anyhow::Result<()> { let code = " export function main(test1?: string, test2: string = \"burkina\", test3: wmill.Resource<'postgres'>, b64: Base64, ls: Base64[], email: Email, literal: \"test\", literal_union: \"test\" | \"test2\", opt_type?: string | null, opt_type_union: string | null, opt_type_union_union2: string | undefined, min_object: {a: string, b: number}) { console.log(42) } "; assert_eq!( parse_deno_signature(code)?, MainArgSignature { star_args: false, star_kwargs: false, args: vec![ Arg { otyp: None, name: "test1".to_string(), typ: Typ::Str(None), default: None, has_default: true }, Arg { otyp: None, name: "test2".to_string(), typ: Typ::Str(None), default: Some(json!("burkina")), has_default: true }, Arg { otyp: None, name: "test3".to_string(), typ: Typ::Resource("postgres".to_string()), default: None, has_default: false }, Arg { otyp: None, name: "b64".to_string(), typ: Typ::Bytes, default: None, has_default: false }, Arg { otyp: None, name: "ls".to_string(), typ: Typ::List(Box::new(Typ::Bytes)), default: None, has_default: false }, Arg { otyp: None, name: "email".to_string(), typ: Typ::Email, default: None, has_default: false }, Arg { otyp: None, name: "literal".to_string(), typ: Typ::Str(Some(vec!["test".to_string()])), default: None, has_default: false }, Arg { otyp: None, name: "literal_union".to_string(), typ: Typ::Str(Some(vec!["test".to_string(), "test2".to_string()])), default: None, has_default: false }, Arg { otyp: None, name: "opt_type".to_string(), typ: Typ::Str(None), default: None, has_default: true }, Arg { otyp: None, name: "opt_type_union".to_string(), typ: Typ::Str(None), default: None, has_default: true }, Arg { otyp: None, name: "opt_type_union_union2".to_string(), typ: Typ::Str(None), default: None, has_default: true }, Arg { otyp: None, name: "min_object".to_string(), typ: Typ::Object(vec![ ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) }, ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Float) } ]), default: None, has_default: false } ] } ); Ok(()) } #[test] fn test_parse_deno_sig_implicit_types() -> anyhow::Result<()> { let code = " export function main(test2 = \"burkina\", bool = true, float = 4.2, int = 42, ls = [\"test\"], min_object = {a: \"test\", b: 42}) { console.log(42) } "; assert_eq!( parse_deno_signature(code)?, MainArgSignature { star_args: false, star_kwargs: false, args: vec![ Arg { otyp: None, name: "test2".to_string(), typ: Typ::Str(None), default: Some(json!("burkina")), has_default: true }, Arg { otyp: None, name: "bool".to_string(), typ: Typ::Bool, default: Some(json!(true)), has_default: true }, Arg { otyp: None, name: "float".to_string(), typ: Typ::Float, default: Some(json!(4.2)), has_default: true }, Arg { otyp: None, name: "int".to_string(), typ: Typ::Int, default: Some(json!(42)), has_default: true }, Arg { otyp: None, name: "ls".to_string(), typ: Typ::List(Box::new(Typ::Str(None))), default: Some(json!(["test"])), has_default: true }, Arg { otyp: None, name: "min_object".to_string(), typ: Typ::Object(vec![ ObjectProperty { key: "a".to_string(), typ: Box::new(Typ::Str(None)) }, ObjectProperty { key: "b".to_string(), typ: Box::new(Typ::Int) } ]), default: Some(json!({"a": "test", "b": 42})), has_default: true } ] } ); Ok(()) } }