feat(typescript): Implement Isolated Declaration (#9086)

**Description:**

This PR adds TypeScript Isolated Declarations implementation by forking Deno's implementation.
This commit is contained in:
Donny/강동윤 2024-06-21 18:54:59 +09:00 committed by GitHub
parent 568b3b37a5
commit a8551592b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1515 additions and 68 deletions

View File

@ -587,7 +587,7 @@ jobs:
- crate: testing_macros
os: ubuntu-latest
runner: ubuntu-latest
- crate: xtask
- crate: swc_typescript
os: ubuntu-latest
runner: ubuntu-latest
steps:

13
Cargo.lock generated
View File

@ -3561,9 +3561,9 @@ dependencies = [
[[package]]
name = "sourcemap"
version = "8.0.0"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf0b8c0d9d32f81aa0ab2b68ab634d9bbce287423606656fddb456ac8601aec3"
checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4"
dependencies = [
"base64-simd",
"bitvec",
@ -5299,6 +5299,15 @@ dependencies = [
[[package]]
name = "swc_typescript"
version = "0.1.0"
dependencies = [
"swc_atoms",
"swc_common",
"swc_ecma_ast",
"swc_ecma_codegen",
"swc_ecma_parser",
"testing",
"thiserror",
]
[[package]]
name = "swc_visit"

View File

@ -109,7 +109,7 @@ resolver = "2"
siphasher = "0.3.9"
smallvec = "1.8.0"
smartstring = "1"
sourcemap = "8.0.0"
sourcemap = "8.0.1"
st-map = "0.2.0"
syn = "2"
tempfile = "3.6.0"

View File

@ -6,7 +6,6 @@
"x",
"Error"
],
"rangeMappings": ";;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -3,7 +3,6 @@
"names": [
"foo"
],
"rangeMappings": ";;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -3,7 +3,6 @@
"names": [
"foo"
],
"rangeMappings": ";;",
"sources": [
"../../input/index.js"
],

View File

@ -7,7 +7,6 @@
"console",
"log"
],
"rangeMappings": ";;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -3,7 +3,6 @@
"names": [
"a"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -3,7 +3,6 @@
"names": [
"a"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -3,7 +3,6 @@
"names": [
"a"
],
"rangeMappings": ";;;",
"sources": [
"../../input/index.js"
],

View File

@ -8,7 +8,6 @@
"resolve",
"setTimeout"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -15,7 +15,6 @@
"customElements",
"define"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -4,7 +4,6 @@
"a",
"b"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -8,4 +8,4 @@ define([
});
});
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJyYW5nZU1hcHBpbmdzIjoiIiwibWFwcGluZ3MiOiIifQ==
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9

View File

@ -3,7 +3,6 @@
"names": [
"test"
],
"rangeMappings": ";;;",
"sources": [
"../../input/index.js"
],

View File

@ -3,7 +3,6 @@
"names": [
"test"
],
"rangeMappings": ";;;;",
"sources": [
"../../input/index.js"
],

View File

@ -4,7 +4,6 @@
"console",
"log"
],
"rangeMappings": ";;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -6,7 +6,6 @@
"foo",
"Base"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/source/a/a.ts"
],

View File

@ -10,7 +10,6 @@
"toBe",
"foo"
],
"rangeMappings": ";;;;;;;;;;",
"sources": [
"../../input/source/a/a.spec.ts"
],

View File

@ -1,7 +1,6 @@
{
"mappings": ";;;;;uBAAc;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA;uBACA",
"names": [],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -7,7 +7,6 @@
"document",
"getElementById"
],
"rangeMappings": ";;;;",
"sources": [
"../../input/index.tsx"
],

View File

@ -1,3 +1,3 @@
var a = "//# sourceMappingURL=[file].map";
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL2lucHV0LzEuanMiXSwic291cmNlc0NvbnRlbnQiOlsidmFyIGEgPSBcIi8vIyBzb3VyY2VNYXBwaW5nVVJMPVtmaWxlXS5tYXBcIlxuIl0sIm5hbWVzIjpbImEiXSwicmFuZ2VNYXBwaW5ncyI6IiIsIm1hcHBpbmdzIjoiQUFBQSxJQUFJQSxJQUFJIn0=
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL2lucHV0LzEuanMiXSwic291cmNlc0NvbnRlbnQiOlsidmFyIGEgPSBcIi8vIyBzb3VyY2VNYXBwaW5nVVJMPVtmaWxlXS5tYXBcIlxuIl0sIm5hbWVzIjpbImEiXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLElBQUkifQ==

View File

@ -9,7 +9,6 @@
"expect",
"toContain"
],
"rangeMappings": ";;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -7,7 +7,6 @@
"stack",
"split"
],
"rangeMappings": ";;;;;;;;;;;;;;;",
"sources": [
"../../input/util.ts"
],

View File

@ -13,4 +13,4 @@ const Button = (0, _linaria.css)`
color: red;
`;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL2lucHV0L2luZGV4LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNzcyB9IGZyb20gXCJsaW5hcmlhXCI7XG5cbmV4cG9ydCBjb25zdCBCdXR0b24gPSBjc3NgXG4gICAgY29sb3I6IHJlZDtcbmA7XG4iXSwibmFtZXMiOlsiQnV0dG9uIiwiY3NzIl0sInJhbmdlTWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7IiwibWFwcGluZ3MiOiI7Ozs7K0JBRWFBOzs7ZUFBQUE7Ozt5QkFGTztBQUViLE1BQU1BLFNBQVNDLElBQUFBLFlBQUcsQ0FBQSxDQUFDOztBQUUxQixDQUFDIn0=
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL2lucHV0L2luZGV4LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNzcyB9IGZyb20gXCJsaW5hcmlhXCI7XG5cbmV4cG9ydCBjb25zdCBCdXR0b24gPSBjc3NgXG4gICAgY29sb3I6IHJlZDtcbmA7XG4iXSwibmFtZXMiOlsiQnV0dG9uIiwiY3NzIl0sIm1hcHBpbmdzIjoiOzs7OytCQUVhQTs7O2VBQUFBOzs7eUJBRk87QUFFYixNQUFNQSxTQUFTQyxJQUFBQSxZQUFHLENBQUEsQ0FBQzs7QUFFMUIsQ0FBQyJ9

View File

@ -42,7 +42,6 @@
"prop",
"get"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/box-model.ts"
],

View File

@ -4,7 +4,6 @@
"foo",
"arr"
],
"rangeMappings": ";;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -8,7 +8,6 @@
"getStaticProps",
"props"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -8,7 +8,6 @@
"getStaticProps",
"props"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -25,7 +25,6 @@
"s",
"_N_E"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -27,7 +27,6 @@
"isSharp",
"toBeLessThan"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -27,7 +27,6 @@
"isSharp",
"toBeLessThan"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -6,7 +6,6 @@
"console",
"log"
],
"rangeMappings": ";;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -6,7 +6,6 @@
"expect",
"toBe"
],
"rangeMappings": ";;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -6,7 +6,6 @@
"Error",
"stack"
],
"rangeMappings": ";;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -1,7 +1,6 @@
{
"mappings": "AAAA,cAAc,iBAAiB;AAC/B,cAAc,wBAAwB;AAEtC,cAAc,WAAW;AACzB,cAAc,eAAe",
"names": [],
"rangeMappings": ";;;",
"sources": [
"../../input/index.ts"
],

View File

@ -29,7 +29,6 @@
"deleteComment",
"delete"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/CommentControlller.ts"
],

View File

@ -51,7 +51,6 @@
"$pull",
"name"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/CommentService.ts"
],

View File

@ -48,7 +48,6 @@
"createPostComment",
"createCommentDto"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/PistController.ts"
],

View File

@ -31,7 +31,6 @@
"user",
"id"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/UserController.ts"
],

View File

@ -6,7 +6,6 @@
"expect",
"toBe"
],
"rangeMappings": ";;;;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -7,7 +7,6 @@
"toBe",
"str"
],
"rangeMappings": ";;;;;;;;;;;;;",
"sources": [
"../../input/index.js"
],

View File

@ -5,7 +5,6 @@
"Symbol",
"for"
],
"rangeMappings": ";;",
"sources": [
"../../input/index.js"
],

View File

@ -1,7 +1,6 @@
{
"mappings": "AAAA,cAAc,iBAAiB;AAC/B,cAAc,wBAAwB;AACtC,cAAc,eAAe;AAC7B,cAAc,WAAW;AACzB,cAAc,eAAe",
"names": [],
"rangeMappings": ";;;;",
"sources": [
"../../input/index.ts"
],

View File

@ -3,7 +3,6 @@
"names": [
"a"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -3,7 +3,6 @@
"names": [
"a"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -45,7 +45,6 @@
"variant",
"constructor"
],
"rangeMappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
"sources": [
"../../input/index.ts"
],

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@
"message",
"bbb"
],
"rangeMappings": "",
"sources": [
"unknown"
],

View File

@ -7,7 +7,6 @@
"message",
"bbb"
],
"rangeMappings": "",
"sources": [
"../../input/index.js"
],

View File

@ -5,7 +5,6 @@
"console",
"log"
],
"rangeMappings": ";",
"sources": [
"$DIR/tests/minify/issue-7475/1-with-preamble/input.js"
],

View File

@ -5,7 +5,6 @@
"console",
"log"
],
"rangeMappings": "",
"sources": [
"$DIR/tests/minify/issue-7475/2-no-preamble/input.js"
],

View File

@ -6,7 +6,6 @@
"aób",
"a"
],
"rangeMappings": "",
"sources": [
"input-preprocess.js"
],

View File

@ -6,7 +6,6 @@
"console",
"log"
],
"rangeMappings": "",
"sources": [
"$DIR/tests/minify/issue-7475/issue-8372/2/input.js"
],

View File

@ -49,6 +49,25 @@ impl Take for ModuleDecl {
}
}
/// Default exports other than **direct** function expression or class
/// expression.
///
///
/// # Note
///
/// ```ts
/// export default function Foo() {
/// }
/// ```
///
/// is [`ExportDefaultDecl`] and it's hoisted.
///
/// ```ts
/// export default (function Foo() {
/// })
/// ```
///
/// is [`ExportDefaultExpr`] and it's not hoisted.
#[ast_node("ExportDefaultExpression")]
#[derive(Eq, Hash, EqIgnoreSpan)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]

View File

@ -9,3 +9,14 @@ repository.workspace = true
version = "0.1.0"
[dependencies]
thiserror = { workspace = true }
swc_atoms = { version = "0.6.7", path = "../swc_atoms" }
swc_common = { version = "0.34.2", path = "../swc_common" }
swc_ecma_ast = { version = "0.115.1", path = "../swc_ecma_ast" }
[dev-dependencies]
swc_ecma_codegen = { version = "0.151.0", path = "../swc_ecma_codegen" }
swc_ecma_parser = { version = "0.146.2", path = "../swc_ecma_parser" }
testing = { version = "0.36.0", path = "../testing" }

View File

@ -0,0 +1,35 @@
use std::sync::Arc;
use swc_common::{FileName, Span};
#[derive(Debug, Clone)]
pub struct SourceRange {
pub filename: Arc<FileName>,
pub range: Span,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum DtsIssue {
#[error("unable to infer type from expression or declaration")]
UnableToInferType { range: SourceRange },
#[error("unable to infer type, falling back to any type")]
UnableToInferTypeFallbackAny { range: SourceRange },
#[error("unable to infer type from object property, skipping")]
UnableToInferTypeFromProp { range: SourceRange },
#[error("unable to infer type from spread, skipping")]
UnableToInferTypeFromSpread { range: SourceRange },
#[error("cannot infer type from using, skipping")]
UnsupportedUsing { range: SourceRange },
}
impl DtsIssue {
pub fn range(&self) -> Option<&SourceRange> {
match self {
DtsIssue::UnableToInferType { range } => Some(range),
DtsIssue::UnableToInferTypeFallbackAny { range } => Some(range),
DtsIssue::UnableToInferTypeFromProp { range } => Some(range),
DtsIssue::UnableToInferTypeFromSpread { range } => Some(range),
DtsIssue::UnsupportedUsing { range } => Some(range),
}
}
}

View File

@ -0,0 +1,929 @@
use std::{mem::take, sync::Arc};
use swc_atoms::Atom;
use swc_common::{util::take::Take, FileName, Span, Spanned, DUMMY_SP};
use swc_ecma_ast::{
BindingIdent, ClassMember, Decl, DefaultDecl, ExportDecl, ExportDefaultDecl, ExportDefaultExpr,
Expr, FnDecl, FnExpr, Ident, Lit, MethodKind, Module, ModuleDecl, ModuleItem, OptChainBase,
Pat, Prop, PropName, PropOrSpread, Stmt, TsEntityName, TsFnOrConstructorType, TsFnParam,
TsFnType, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsNamespaceBody,
TsPropertySignature, TsTupleElement, TsTupleType, TsType, TsTypeAnn, TsTypeElement, TsTypeLit,
TsTypeOperator, TsTypeOperatorOp, TsTypeRef, VarDecl, VarDeclKind, VarDeclarator,
};
use crate::diagnostic::{DtsIssue, SourceRange};
/// TypeScript Isolated Declaration support.
///
/// ---
///
/// # License
///
/// Mostly copied from <https://github.com/denoland/deno_graph/blob/15db6e5fb6d3faea027e16c3d9ce6498b11beed2/src/fast_check/transform_dts.rs>
///
/// The original code is MIT licensed.
pub struct FastDts {
filename: Arc<FileName>,
is_top_level: bool,
id_counter: u32,
diagnostics: Vec<DtsIssue>,
}
/// Diagnostics
impl FastDts {
pub fn new(filename: Arc<FileName>) -> Self {
Self {
filename,
is_top_level: false,
id_counter: 0,
diagnostics: vec![],
}
}
fn mark_diagnostic(&mut self, diagnostic: DtsIssue) {
self.diagnostics.push(diagnostic)
}
fn source_range_to_range(&self, range: Span) -> SourceRange {
SourceRange {
filename: self.filename.clone(),
range,
}
}
fn mark_diagnostic_unable_to_infer(&mut self, range: Span) {
self.mark_diagnostic(DtsIssue::UnableToInferType {
range: self.source_range_to_range(range),
})
}
fn mark_diagnostic_any_fallback(&mut self, range: Span) {
self.mark_diagnostic(DtsIssue::UnableToInferTypeFallbackAny {
range: self.source_range_to_range(range),
})
}
fn mark_diagnostic_unsupported_prop(&mut self, range: Span) {
self.mark_diagnostic(DtsIssue::UnableToInferTypeFromProp {
range: self.source_range_to_range(range),
})
}
}
impl FastDts {
pub fn transform(&mut self, module: &mut Module) -> Vec<DtsIssue> {
self.is_top_level = true;
self.transform_module_items(&mut module.body);
take(&mut self.diagnostics)
}
fn transform_module_items(&mut self, items: &mut Vec<ModuleItem>) {
let orig_items = take(items);
let mut new_items = Vec::with_capacity(orig_items.len());
let mut prev_is_overload = false;
for mut item in orig_items {
let is_overload = match &item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. }))
| ModuleItem::Stmt(Stmt::Decl(decl)) => match decl {
Decl::Fn(FnDecl {
function, declare, ..
}) => !declare && function.body.is_none(),
_ => false,
},
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
decl,
..
})) => {
matches!(decl, DefaultDecl::Fn(FnExpr { function, .. }) if function.body.is_none())
}
_ => false,
};
match &mut item {
// Keep all these
ModuleItem::ModuleDecl(
ModuleDecl::Import(..)
| ModuleDecl::TsImportEquals(_)
| ModuleDecl::TsNamespaceExport(_)
| ModuleDecl::TsExportAssignment(_)
| ModuleDecl::ExportNamed(_)
| ModuleDecl::ExportAll(_),
) => new_items.push(item),
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
span, decl, ..
})) => {
let should_keep = prev_is_overload && !is_overload;
if should_keep {
prev_is_overload = is_overload;
continue;
}
if let Some(()) = self.decl_to_type_decl(decl) {
new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(
ExportDecl {
decl: decl.take(),
span: *span,
},
)));
} else {
self.mark_diagnostic(DtsIssue::UnableToInferType {
range: self.source_range_to_range(*span),
})
}
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) => {
match &mut export.decl {
DefaultDecl::Class(class_expr) => {
self.class_body_to_type(&mut class_expr.class.body);
}
DefaultDecl::Fn(fn_expr) => {
fn_expr.function.body = None;
}
DefaultDecl::TsInterfaceDecl(_) => {}
};
let should_keep = prev_is_overload && !is_overload;
prev_is_overload = is_overload;
if should_keep {
continue;
}
new_items.push(item);
}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) => {
let should_keep = prev_is_overload && !is_overload;
prev_is_overload = is_overload;
if should_keep {
continue;
}
let name = self.gen_unique_name();
let name_ident = Ident::new(name, DUMMY_SP);
let type_ann = self
.expr_to_ts_type(export.expr.clone(), false, true)
.map(type_ann);
if let Some(type_ann) = type_ann {
new_items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(
VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Const,
declare: true,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: name_ident.clone(),
type_ann: Some(type_ann),
}),
init: None,
definite: false,
}],
},
)))));
new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
span: export.span,
expr: Box::new(Expr::Ident(name_ident)),
},
)))
} else {
new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
span: export.span,
expr: export.expr.take(),
},
)))
}
}
ModuleItem::Stmt(Stmt::Decl(decl)) => match decl {
Decl::TsEnum(_)
| Decl::Class(_)
| Decl::Fn(_)
| Decl::Var(_)
| Decl::TsModule(_) => {
if let Some(()) = self.decl_to_type_decl(decl) {
new_items.push(item);
} else {
self.mark_diagnostic_unable_to_infer(decl.span())
}
}
Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::Using(_) => {
new_items.push(item);
}
},
ModuleItem::Stmt(..) => {}
}
prev_is_overload = is_overload;
}
*items = new_items;
}
fn expr_to_ts_type(
&mut self,
e: Box<Expr>,
as_const: bool,
as_readonly: bool,
) -> Option<Box<TsType>> {
match *e {
Expr::Array(arr) => {
let mut elem_types: Vec<TsTupleElement> = vec![];
for elems in arr.elems {
if let Some(expr_or_spread) = elems {
let span = expr_or_spread.span();
if let Some(ts_expr) =
self.expr_to_ts_type(expr_or_spread.expr, as_const, as_readonly)
{
elem_types.push(ts_tuple_element(ts_expr));
} else {
self.mark_diagnostic_unable_to_infer(span);
}
} else {
// TypeScript converts holey arrays to any
// Example: const a = [,,] -> const a = [any, any, any]
elem_types.push(ts_tuple_element(Box::new(TsType::TsKeywordType(
TsKeywordType {
kind: TsKeywordTypeKind::TsAnyKeyword,
span: DUMMY_SP,
},
))))
}
}
let mut result = Box::new(TsType::TsTupleType(TsTupleType {
span: arr.span,
elem_types,
}));
if as_readonly {
result = ts_readonly(result);
}
Some(result)
}
Expr::Object(obj) => {
let mut members: Vec<TsTypeElement> = vec![];
// TODO: Prescan all object properties to know which ones
// have a getter or a setter. This allows us to apply
// TypeScript's `readonly` keyword accordingly.
for item in obj.props {
match item {
PropOrSpread::Prop(prop_box) => {
let prop = *prop_box;
match prop {
Prop::KeyValue(key_value) => {
let (key, computed) = match key_value.key {
PropName::Ident(ident) => (Expr::Ident(ident), false),
PropName::Str(str_prop) => {
(Expr::Lit(Lit::Str(str_prop)), false)
}
PropName::Num(num) => (Expr::Lit(Lit::Num(num)), true),
PropName::Computed(computed) => (*computed.expr, true),
PropName::BigInt(big_int) => {
(Expr::Lit(Lit::BigInt(big_int)), true)
}
};
let init_type = self
.expr_to_ts_type(key_value.value, as_const, as_readonly)
.map(type_ann);
members.push(TsTypeElement::TsPropertySignature(
TsPropertySignature {
span: DUMMY_SP,
readonly: as_readonly,
key: Box::new(key),
computed,
optional: false,
type_ann: init_type,
},
));
}
Prop::Shorthand(_)
| Prop::Assign(_)
| Prop::Getter(_)
| Prop::Setter(_)
| Prop::Method(_) => {
self.mark_diagnostic_unsupported_prop(prop.span());
}
}
}
PropOrSpread::Spread(_) => {
self.mark_diagnostic(DtsIssue::UnableToInferTypeFromSpread {
range: self.source_range_to_range(item.span()),
})
}
}
}
Some(Box::new(TsType::TsTypeLit(TsTypeLit {
span: obj.span,
members,
})))
}
Expr::Lit(lit) => {
if as_const {
maybe_lit_to_ts_type_const(&lit)
} else {
maybe_lit_to_ts_type(&lit)
}
}
Expr::TsConstAssertion(ts_const) => self.expr_to_ts_type(ts_const.expr, true, true),
Expr::TsSatisfies(satisifies) => {
self.expr_to_ts_type(satisifies.expr, as_const, as_readonly)
}
Expr::TsAs(ts_as) => Some(ts_as.type_ann),
Expr::Fn(fn_expr) => {
let return_type = fn_expr
.function
.return_type
.map_or(any_type_ann(), |val| val);
let params: Vec<TsFnParam> = fn_expr
.function
.params
.into_iter()
.filter_map(|param| self.pat_to_ts_fn_param(param.pat))
.collect();
Some(Box::new(TsType::TsFnOrConstructorType(
TsFnOrConstructorType::TsFnType(TsFnType {
span: fn_expr.function.span,
params,
type_ann: return_type,
type_params: fn_expr.function.type_params,
}),
)))
}
Expr::Arrow(arrow_expr) => {
let return_type = arrow_expr.return_type.map_or(any_type_ann(), |val| val);
let params = arrow_expr
.params
.into_iter()
.filter_map(|pat| self.pat_to_ts_fn_param(pat))
.collect();
Some(Box::new(TsType::TsFnOrConstructorType(
TsFnOrConstructorType::TsFnType(TsFnType {
span: arrow_expr.span,
params,
type_ann: return_type,
type_params: arrow_expr.type_params,
}),
)))
}
// Since fast check requires explicit type annotations these
// can be dropped as they are not part of an export declaration
Expr::This(_)
| Expr::Unary(_)
| Expr::Update(_)
| Expr::Bin(_)
| Expr::Assign(_)
| Expr::Member(_)
| Expr::SuperProp(_)
| Expr::Cond(_)
| Expr::Call(_)
| Expr::New(_)
| Expr::Seq(_)
| Expr::Ident(_)
| Expr::Tpl(_)
| Expr::TaggedTpl(_)
| Expr::Class(_)
| Expr::Yield(_)
| Expr::MetaProp(_)
| Expr::Await(_)
| Expr::Paren(_)
| Expr::JSXMember(_)
| Expr::JSXNamespacedName(_)
| Expr::JSXEmpty(_)
| Expr::JSXElement(_)
| Expr::JSXFragment(_)
| Expr::TsTypeAssertion(_)
| Expr::TsNonNull(_)
| Expr::TsInstantiation(_)
| Expr::PrivateName(_)
| Expr::OptChain(_)
| Expr::Invalid(_) => None,
}
}
fn decl_to_type_decl(&mut self, decl: &mut Decl) -> Option<()> {
let is_declare = self.is_top_level;
match decl {
Decl::Class(class_decl) => {
self.class_body_to_type(&mut class_decl.class.body);
class_decl.declare = is_declare;
Some(())
}
Decl::Fn(fn_decl) => {
fn_decl.function.body = None;
fn_decl.declare = is_declare;
for param in &mut fn_decl.function.params {
match &mut param.pat {
Pat::Ident(ident) => {
if ident.type_ann.is_none() {
self.mark_diagnostic_any_fallback(ident.span());
ident.type_ann = Some(any_type_ann());
}
}
Pat::Assign(assign_pat) => {
match &mut *assign_pat.left {
Pat::Ident(ident) => {
if ident.type_ann.is_none() {
ident.type_ann = self.infer_expr_fallback_any(
assign_pat.right.take(),
false,
false,
);
}
ident.optional = true;
param.pat = Pat::Ident(ident.clone());
}
Pat::Array(arr_pat) => {
if arr_pat.type_ann.is_none() {
arr_pat.type_ann = self.infer_expr_fallback_any(
assign_pat.right.take(),
false,
false,
);
}
arr_pat.optional = true;
param.pat = Pat::Array(arr_pat.clone());
}
Pat::Object(obj_pat) => {
if obj_pat.type_ann.is_none() {
obj_pat.type_ann = self.infer_expr_fallback_any(
assign_pat.right.take(),
false,
false,
);
}
obj_pat.optional = true;
param.pat = Pat::Object(obj_pat.clone());
}
Pat::Rest(_) | Pat::Assign(_) | Pat::Expr(_) | Pat::Invalid(_) => {}
};
}
Pat::Array(_)
| Pat::Rest(_)
| Pat::Object(_)
| Pat::Invalid(_)
| Pat::Expr(_) => {}
}
}
Some(())
}
Decl::Var(var_decl) => {
var_decl.declare = is_declare;
for decl in &mut var_decl.decls {
if let Pat::Ident(ident) = &mut decl.name {
if ident.type_ann.is_some() {
decl.init = None;
continue;
}
let ts_type = decl
.init
.take()
.and_then(|init| self.expr_to_ts_type(init, false, true))
.map(type_ann)
.or_else(|| {
self.mark_diagnostic_any_fallback(ident.span());
Some(any_type_ann())
});
ident.type_ann = ts_type;
} else {
self.mark_diagnostic_unable_to_infer(decl.span());
}
decl.init = None;
}
Some(())
}
Decl::TsEnum(ts_enum) => {
ts_enum.declare = is_declare;
for member in &mut ts_enum.members {
if let Some(init) = &member.init {
// Support for expressions is limited in enums,
// see https://www.typescriptlang.org/docs/handbook/enums.html
member.init = if self.valid_enum_init_expr(init) {
Some(init.clone())
} else {
None
};
}
}
Some(())
}
Decl::TsModule(ts_module) => {
ts_module.declare = is_declare;
if let Some(body) = ts_module.body.take() {
ts_module.body = Some(self.transform_ts_ns_body(body));
Some(())
} else {
Some(())
}
}
Decl::TsInterface(_) | Decl::TsTypeAlias(_) => Some(()),
Decl::Using(_) => {
self.mark_diagnostic(DtsIssue::UnsupportedUsing {
range: self.source_range_to_range(decl.span()),
});
None
}
}
}
fn transform_ts_ns_body(&mut self, ns: TsNamespaceBody) -> TsNamespaceBody {
let original_is_top_level = self.is_top_level;
self.is_top_level = false;
let body = match ns {
TsNamespaceBody::TsModuleBlock(mut ts_module_block) => {
self.transform_module_items(&mut ts_module_block.body);
TsNamespaceBody::TsModuleBlock(ts_module_block)
}
TsNamespaceBody::TsNamespaceDecl(ts_ns) => self.transform_ts_ns_body(*ts_ns.body),
};
self.is_top_level = original_is_top_level;
body
}
// Support for expressions is limited in enums,
// see https://www.typescriptlang.org/docs/handbook/enums.html
fn valid_enum_init_expr(&mut self, expr: &Expr) -> bool {
match expr {
Expr::Bin(bin_expr) => {
if !self.valid_enum_init_expr(&bin_expr.left) {
false
} else {
self.valid_enum_init_expr(&bin_expr.right)
}
}
Expr::Member(member_expr) => self.valid_enum_init_expr(&member_expr.obj),
Expr::OptChain(opt_expr) => match &*opt_expr.base {
OptChainBase::Member(member_expr) => {
self.valid_enum_init_expr(&Expr::Member(member_expr.clone()))
}
OptChainBase::Call(_) => false,
},
// TS does infer the type of identifiers
Expr::Ident(_) => true,
Expr::Lit(lit) => match lit {
Lit::Num(_) | Lit::Str(_) => true,
Lit::Bool(_) | Lit::Null(_) | Lit::BigInt(_) | Lit::Regex(_) | Lit::JSXText(_) => {
false
}
},
Expr::Tpl(tpl_expr) => {
for expr in &tpl_expr.exprs {
if !self.valid_enum_init_expr(expr) {
return false;
}
}
true
}
Expr::Paren(paren_expr) => self.valid_enum_init_expr(&paren_expr.expr),
Expr::TsTypeAssertion(ts_ass) => {
// Only assertions to number are allowed for computed
// enum members.
match &*ts_ass.type_ann {
TsType::TsLitType(ts_lit) => match ts_lit.lit {
TsLit::Number(_) => true,
TsLit::Str(_) | TsLit::Bool(_) | TsLit::BigInt(_) | TsLit::Tpl(_) => false,
},
TsType::TsKeywordType(_)
| TsType::TsThisType(_)
| TsType::TsFnOrConstructorType(_)
| TsType::TsTypeRef(_)
| TsType::TsTypeQuery(_)
| TsType::TsTypeLit(_)
| TsType::TsArrayType(_)
| TsType::TsTupleType(_)
| TsType::TsOptionalType(_)
| TsType::TsRestType(_)
| TsType::TsUnionOrIntersectionType(_)
| TsType::TsConditionalType(_)
| TsType::TsInferType(_)
| TsType::TsParenthesizedType(_)
| TsType::TsTypeOperator(_)
| TsType::TsIndexedAccessType(_)
| TsType::TsMappedType(_)
| TsType::TsTypePredicate(_)
| TsType::TsImportType(_) => false,
}
}
Expr::TsAs(ts_as) => self.valid_enum_ts_type(&ts_as.type_ann),
// These are not valid as enum member initializer and
// TS will throw a type error. For declaration generation
// they will be dropped in TS so we do that too.
Expr::TsInstantiation(_)
| Expr::Call(_)
| Expr::Update(_)
| Expr::PrivateName(_)
| Expr::TsSatisfies(_)
| Expr::TsNonNull(_)
| Expr::TsConstAssertion(_)
| Expr::Cond(_)
| Expr::Seq(_)
| Expr::TaggedTpl(_)
| Expr::Object(_)
| Expr::Array(_)
| Expr::Arrow(_)
| Expr::Class(_)
| Expr::Await(_)
| Expr::MetaProp(_)
| Expr::New(_)
| Expr::JSXMember(_)
| Expr::JSXNamespacedName(_)
| Expr::JSXEmpty(_)
| Expr::JSXElement(_)
| Expr::JSXFragment(_)
| Expr::Unary(_)
| Expr::Assign(_)
| Expr::Yield(_)
| Expr::SuperProp(_)
| Expr::Fn(_)
| Expr::This(_)
| Expr::Invalid(_) => false,
}
}
fn valid_enum_ts_type(&mut self, ts_type: &TsType) -> bool {
match ts_type {
TsType::TsLitType(ts_lit) => match ts_lit.lit {
TsLit::Number(_) => true,
TsLit::Str(_) | TsLit::Bool(_) | TsLit::BigInt(_) | TsLit::Tpl(_) => false,
},
TsType::TsKeywordType(_)
| TsType::TsThisType(_)
| TsType::TsFnOrConstructorType(_)
| TsType::TsTypeRef(_)
| TsType::TsTypeQuery(_)
| TsType::TsTypeLit(_)
| TsType::TsArrayType(_)
| TsType::TsTupleType(_)
| TsType::TsOptionalType(_)
| TsType::TsRestType(_)
| TsType::TsUnionOrIntersectionType(_)
| TsType::TsConditionalType(_)
| TsType::TsInferType(_)
| TsType::TsParenthesizedType(_)
| TsType::TsTypeOperator(_)
| TsType::TsIndexedAccessType(_)
| TsType::TsMappedType(_)
| TsType::TsTypePredicate(_)
| TsType::TsImportType(_) => false,
}
}
fn infer_expr_fallback_any(
&mut self,
expr: Box<Expr>,
as_const: bool,
as_readonly: bool,
) -> Option<Box<TsTypeAnn>> {
let span = expr.span();
if let Some(ts_type) = self.expr_to_ts_type(expr, as_const, as_readonly) {
Some(type_ann(ts_type))
} else {
self.mark_diagnostic_any_fallback(span);
Some(any_type_ann())
}
}
fn class_body_to_type(&mut self, body: &mut Vec<ClassMember>) {
// Track if the previous member was an overload signature or not.
// When overloads are present the last item has the implementation
// body. For declaration files the implementation always needs to
// be dropped. Needs to be unique for each class because another
// class could be created inside a class method.
let mut prev_is_overload = false;
let new_body = body
.take()
.into_iter()
.filter(|member| match member {
ClassMember::Constructor(class_constructor) => {
let is_overload = class_constructor.body.is_none();
if !prev_is_overload || is_overload {
prev_is_overload = is_overload;
true
} else {
prev_is_overload = false;
false
}
}
ClassMember::Method(method) => {
let is_overload = method.function.body.is_none();
if !prev_is_overload || is_overload {
prev_is_overload = is_overload;
true
} else {
prev_is_overload = false;
false
}
}
ClassMember::TsIndexSignature(_)
| ClassMember::ClassProp(_)
| ClassMember::PrivateProp(_)
| ClassMember::Empty(_)
| ClassMember::StaticBlock(_)
| ClassMember::AutoAccessor(_)
| ClassMember::PrivateMethod(_) => {
prev_is_overload = false;
true
}
})
.filter_map(|member| match member {
ClassMember::Constructor(mut class_constructor) => {
class_constructor.body = None;
Some(ClassMember::Constructor(class_constructor))
}
ClassMember::Method(mut method) => {
method.function.body = None;
if method.kind == MethodKind::Setter {
method.function.return_type = None;
}
Some(ClassMember::Method(method))
}
ClassMember::ClassProp(mut prop) => {
if prop.type_ann.is_none() {
if let Some(value) = prop.value {
prop.type_ann = self
.expr_to_ts_type(value, false, false)
.map(type_ann)
.or_else(|| Some(any_type_ann()));
}
}
prop.value = None;
prop.definite = false;
prop.declare = false;
Some(ClassMember::ClassProp(prop))
}
ClassMember::TsIndexSignature(index_sig) => {
Some(ClassMember::TsIndexSignature(index_sig))
}
// These can be removed as they are not relevant for types
ClassMember::PrivateMethod(_)
| ClassMember::PrivateProp(_)
| ClassMember::Empty(_)
| ClassMember::StaticBlock(_)
| ClassMember::AutoAccessor(_) => None,
})
.collect();
*body = new_body;
}
fn pat_to_ts_fn_param(&mut self, pat: Pat) -> Option<TsFnParam> {
match pat {
Pat::Ident(binding_id) => Some(TsFnParam::Ident(binding_id)),
Pat::Array(arr_pat) => Some(TsFnParam::Array(arr_pat)),
Pat::Rest(rest_pat) => Some(TsFnParam::Rest(rest_pat)),
Pat::Object(obj) => Some(TsFnParam::Object(obj)),
Pat::Assign(assign_pat) => {
self.expr_to_ts_type(assign_pat.right, false, false)
.map(|param| {
let name = if let Pat::Ident(ident) = *assign_pat.left {
ident.id.sym.clone()
} else {
self.gen_unique_name()
};
TsFnParam::Ident(BindingIdent {
id: Ident::new(name, assign_pat.span),
type_ann: Some(type_ann(param)),
})
})
}
Pat::Expr(expr) => {
self.mark_diagnostic_unable_to_infer(expr.span());
None
}
// Invalid code is invalid, not sure why SWC doesn't throw
// a parse error here.
Pat::Invalid(_) => None,
}
}
fn gen_unique_name(&mut self) -> Atom {
self.id_counter += 1;
format!("_dts_{}", self.id_counter).into()
}
}
fn any_type_ann() -> Box<TsTypeAnn> {
type_ann(ts_keyword_type(TsKeywordTypeKind::TsAnyKeyword))
}
fn type_ann(ts_type: Box<TsType>) -> Box<TsTypeAnn> {
Box::new(TsTypeAnn {
span: DUMMY_SP,
type_ann: ts_type,
})
}
fn type_ref(name: Atom) -> TsTypeRef {
TsTypeRef {
span: DUMMY_SP,
type_name: TsEntityName::Ident(Ident::new(name, DUMMY_SP)),
type_params: None,
}
}
fn ts_readonly(ann: Box<TsType>) -> Box<TsType> {
Box::new(TsType::TsTypeOperator(TsTypeOperator {
span: DUMMY_SP,
op: TsTypeOperatorOp::ReadOnly,
type_ann: ann,
}))
}
fn ts_tuple_element(ts_type: Box<TsType>) -> TsTupleElement {
TsTupleElement {
label: None,
span: DUMMY_SP,
ty: ts_type,
}
}
fn ts_keyword_type(kind: TsKeywordTypeKind) -> Box<TsType> {
Box::new(TsType::TsKeywordType(TsKeywordType {
span: DUMMY_SP,
kind,
}))
}
fn ts_lit_type(lit: TsLit) -> Box<TsType> {
Box::new(TsType::TsLitType(TsLitType {
lit,
span: DUMMY_SP,
}))
}
fn regex_type() -> Box<TsType> {
Box::new(TsType::TsTypeRef(type_ref("RegExp".into())))
}
fn maybe_lit_to_ts_type_const(lit: &Lit) -> Option<Box<TsType>> {
match lit {
Lit::Str(lit_str) => Some(ts_lit_type(TsLit::Str(lit_str.clone()))),
Lit::Bool(lit_bool) => Some(ts_lit_type(TsLit::Bool(*lit_bool))),
Lit::Null(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsNullKeyword)),
Lit::Num(lit_num) => Some(ts_lit_type(TsLit::Number(lit_num.clone()))),
Lit::BigInt(lit_bigint) => Some(ts_lit_type(TsLit::BigInt(lit_bigint.clone()))),
Lit::Regex(_) => Some(regex_type()),
Lit::JSXText(_) => None,
}
}
fn maybe_lit_to_ts_type(lit: &Lit) -> Option<Box<TsType>> {
match lit {
Lit::Str(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsStringKeyword)),
Lit::Bool(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsBooleanKeyword)),
Lit::Null(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsNullKeyword)),
Lit::Num(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsNumberKeyword)),
Lit::BigInt(_) => Some(ts_keyword_type(TsKeywordTypeKind::TsBigIntKeyword)),
Lit::Regex(_) => Some(regex_type()),
Lit::JSXText(_) => None,
}
}

View File

@ -1,14 +1,4 @@
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#![allow(clippy::boxed_local)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
pub mod diagnostic;
pub mod fast_dts;

View File

@ -0,0 +1,501 @@
//! Tests copied from deno
use std::sync::Arc;
use swc_ecma_ast::EsVersion;
use swc_ecma_codegen::to_code;
use swc_ecma_parser::{parse_file_as_module, Syntax, TsConfig};
use swc_typescript::fast_dts::FastDts;
#[track_caller]
fn transform_dts_test(source: &str, expected: &str) {
testing::run_test(false, |cm, _| {
let fm = cm.new_source_file(
swc_common::FileName::Real("test.ts".into()),
source.to_string(),
);
let mut checker = FastDts::new(Arc::new(fm.name.clone()));
let mut module = parse_file_as_module(
&fm,
Syntax::Typescript(TsConfig {
..Default::default()
}),
EsVersion::latest(),
None,
&mut vec![],
)
.unwrap();
let _issues = checker.transform(&mut module);
let code = to_code(&module);
assert_eq!(
code.trim(),
expected.trim(),
"Actual:\n{code}\nExpected:\n{expected}"
);
Ok(())
})
.unwrap();
}
#[test]
fn dts_function_test() {
transform_dts_test(
r#"export function foo(a: number): number {
return {};
}"#,
"export declare function foo(a: number): number;",
);
transform_dts_test(
r#"export function foo(a: string): number;
export function foo(a: any): number {
return {};
}"#,
r#"export declare function foo(a: string): number;"#,
);
transform_dts_test(
r#"export function foo(a = 2): number {
return 2;
}"#,
r#"export declare function foo(a?: number): number;"#,
);
transform_dts_test(
r#"export function foo(a: string = 2): number {
return 2;
}"#,
r#"export declare function foo(a?: string): number;"#,
);
transform_dts_test(
r#"export function foo([a, b] = [1, 2]): number {
return 2;
}"#,
r#"export declare function foo([a, b]?: [number, number]): number;"#,
);
transform_dts_test(
r#"export function foo({a, b} = { a: 1, b: 2 }): number {
return 2;
}"#,
r#"export declare function foo({ a, b }?: {
a: number;
b: number;
}): number;"#,
);
}
#[test]
fn dts_class_decl_test() {
transform_dts_test(
r#"export class Foo {
a: number = 2;
static b: number = 1;
#b: number = 3;
constructor(value: string) {
return 42;
}
foo(): string {
return "abc";
}
#bar(): number {
return 2
}
get asdf(): number {
}
set asdf(value: number) {
}
static {
}
}"#,
r#"export declare class Foo {
a: number;
static b: number;
constructor(value: string);
foo(): string;
get asdf(): number;
set asdf(value: number);
}"#,
);
}
#[test]
fn dts_class_decl_rest_test() {
transform_dts_test(
r#"export class Foo {
constructor(...args: string[]) {}
}"#,
r#"export declare class Foo {
constructor(...args: string[]);
}"#,
);
}
#[test]
fn dts_class_decl_overloads_test() {
transform_dts_test(
r#"export class Foo {
constructor(arg: string);
constructor(arg: number);
constructor(arg: any) {}
}"#,
r#"export declare class Foo {
constructor(arg: string);
constructor(arg: number);
}"#,
);
transform_dts_test(
r#"export class Foo {
foo(arg: string);
foo(arg: number);
foo(arg: any) {}
}"#,
r#"export declare class Foo {
foo(arg: string);
foo(arg: number);
}"#,
);
transform_dts_test(
r#"export class Foo {
constructor(arg: string);
constructor(arg: number);
constructor(arg: any) {}
bar(arg: number): number {
return 2
}
foo(arg: string);
foo(arg: number);
foo(arg: any) {}
}"#,
r#"export declare class Foo {
constructor(arg: string);
constructor(arg: number);
bar(arg: number): number;
foo(arg: string);
foo(arg: number);
}"#,
);
}
#[test]
fn dts_class_decl_prop_test() {
transform_dts_test(
r#"export class Foo { private a!: string }"#,
r#"export declare class Foo {
private a: string;
}"#,
);
transform_dts_test(
r#"export class Foo { declare a: string }"#,
r#"export declare class Foo {
a: string;
}"#,
);
}
#[test]
fn dts_class_decl_prop_infer_test() {
transform_dts_test(
r#"export class Foo { foo = (a: string): string => ({} as any) }"#,
r#"export declare class Foo {
foo: (a: string) => string;
}"#,
);
transform_dts_test(
r#"export class Foo { foo = function(a: string): void {} }"#,
r#"export declare class Foo {
foo: (a: string) => void;
}"#,
);
}
#[test]
fn dts_var_decl_test() {
transform_dts_test(
r#"export const foo: number = 42;"#,
"export declare const foo: number;",
);
transform_dts_test(
r#"export var foo: number = 42;"#,
"export declare var foo: number;",
);
transform_dts_test(
r#"export let foo: number = 42;"#,
"export declare let foo: number;",
);
// Default to any if it cannot be determined
transform_dts_test(
r#"export const foo = adsf.b;"#,
"export declare const foo: any;",
);
transform_dts_test(r#"export let foo;"#, "export declare let foo: any;");
}
#[test]
fn dts_global_declare() {
transform_dts_test(
r#"declare global {
interface String {
fancyFormat(opts: StringFormatOptions): string;
}
}"#,
r#"declare global {
interface String {
fancyFormat(opts: StringFormatOptions): string;
}
}"#,
);
}
#[test]
fn dts_inference() {
transform_dts_test(
r#"export const foo = null as string as number;"#,
"export declare const foo: number;",
);
}
#[test]
fn dts_as_const() {
transform_dts_test(
r#"export const foo = [1, 2] as const;"#,
"export declare const foo: readonly [1, 2];",
);
transform_dts_test(
r#"export const foo = [1, ,2] as const;"#,
"export declare const foo: readonly [1, any, 2];",
);
transform_dts_test(
r#"export const foo = { str: "bar", bool: true, bool2: false, num: 42, nullish: null } as const;"#,
r#"export declare const foo: {
readonly str: "bar";
readonly bool: true;
readonly bool2: false;
readonly num: 42;
readonly nullish: null;
};"#,
);
transform_dts_test(
r#"export const foo = { str: [1, 2] as const } as const;"#,
r#"export declare const foo: {
readonly str: readonly [1, 2];
};"#,
);
// TODO: Requires type resolving
transform_dts_test(
r#"export const foo = { bar } as const;"#,
r#"export declare const foo: {
};"#,
);
}
#[test]
fn dts_literal_inference_ann() {
transform_dts_test(
r#"export const foo: number = "abc";"#,
"export declare const foo: number;",
);
transform_dts_test(
r#"export let foo: number = "abc";"#,
"export declare let foo: number;",
);
transform_dts_test(
r#"export var foo: number = "abc";"#,
"export declare var foo: number;",
);
}
#[test]
fn dts_literal_inference() {
transform_dts_test(
r#"export const foo = 42;"#,
"export declare const foo: number;",
);
transform_dts_test(
r#"export const foo = "foo";"#,
"export declare const foo: string;",
);
transform_dts_test(
r#"export const foo = true;"#,
"export declare const foo: boolean;",
);
transform_dts_test(
r#"export const foo = false;"#,
"export declare const foo: boolean;",
);
transform_dts_test(
r#"export const foo = null;"#,
"export declare const foo: null;",
);
transform_dts_test(
r#"export let foo = undefined;"#,
"export declare let foo: any;",
);
transform_dts_test(
r#"export let foo = 10n;"#,
"export declare let foo: bigint;",
);
transform_dts_test(
r#"export let foo = /foo/;"#,
"export declare let foo: RegExp;",
);
}
#[test]
fn dts_fn_expr() {
transform_dts_test(
r#"export let foo = function add(a: number, b: number): number {
return a + b;
}"#,
"export declare let foo: (a: number, b: number) => number;",
);
transform_dts_test(
r#"export let foo = function add<T>([a, b]: T): void {}"#,
"export declare let foo: <T>([a, b]: T) => void;",
);
transform_dts_test(
r#"export let foo = function add<T>({a, b}: T): void {}"#,
"export declare let foo: <T>({ a, b }: T) => void;",
);
transform_dts_test(
r#"export let foo = function add(a = 2): void {}"#,
"export declare let foo: (a: number) => void;",
);
transform_dts_test(
r#"export let foo = function add(...params: any[]): void {}"#,
"export declare let foo: (...params: any[]) => void;",
);
}
#[test]
fn dts_fn_arrow_expr() {
transform_dts_test(
r#"export let foo = (a: number, b: number): number => {
return a + b;
}"#,
"export declare let foo: (a: number, b: number) => number;",
);
transform_dts_test(
r#"export let foo = <T>([a, b]: T): void => {}"#,
"export declare let foo: <T>([a, b]: T) => void;",
);
transform_dts_test(
r#"export let foo = <T>({a, b}: T): void => {}"#,
"export declare let foo: <T>({ a, b }: T) => void;",
);
transform_dts_test(
r#"export let foo = (a = 2): void => {}"#,
"export declare let foo: (a: number) => void;",
);
transform_dts_test(
r#"export let foo = (...params: any[]): void => {}"#,
"export declare let foo: (...params: any[]) => void;",
);
}
#[test]
fn dts_type_export() {
transform_dts_test(r#"interface Foo {}"#, "interface Foo {\n}");
transform_dts_test(r#"type Foo = number;"#, "type Foo = number;");
transform_dts_test(r#"export interface Foo {}"#, "export interface Foo {\n}");
transform_dts_test(r#"export type Foo = number;"#, "export type Foo = number;");
}
#[test]
fn dts_enum_export() {
transform_dts_test(
r#"export enum Foo { A, B }"#,
"export declare enum Foo {\n A,\n B\n}",
);
transform_dts_test(
r#"export const enum Foo { A, B }"#,
"export declare const enum Foo {\n A,\n B\n}",
);
transform_dts_test(
r#"export enum Foo { A = "foo", B = "bar" }"#,
r#"export declare enum Foo {
A = "foo",
B = "bar"
}"#,
);
// TODO: Enum rules https://www.typescriptlang.org/docs/handbook/enums.html
}
#[test]
fn dts_default_export() {
transform_dts_test(
r#"export default function(a: number, b: number): number {};"#,
"export default function(a: number, b: number): number;",
);
transform_dts_test(
r#"export default function(a: number, b: number): number;
export default function(a: number, b: number): any {
return foo
};"#,
"export default function(a: number, b: number): number;",
);
transform_dts_test(
r#"export default class {foo = 2};"#,
r#"export default class {
foo: number;
}"#,
);
transform_dts_test(
r#"export default 42;"#,
r#"declare const _dts_1: number;
export default _dts_1;"#,
);
// TODO
// transform_dts_test(
// r#"const a: number = 42; export default a;"#,
// r#"declare const a: number;
// export default a;"#,
// )
// ;
}
#[test]
fn dts_default_export_named() {
transform_dts_test(
r#"export { foo, bar } from "foo";"#,
r#"export { foo, bar } from "foo";"#,
);
}
#[test]
fn dts_default_export_all() {
transform_dts_test(
r#"export * as foo from "foo";"#,
r#"export * as foo from "foo";"#,
);
}
#[test]
fn dts_imports() {
transform_dts_test(
r#"import { foo } from "bar";export const foobar = foo;"#,
r#"import { foo } from "bar";
export declare const foobar: any;"#,
);
}