Skip to content

Commit

Permalink
feat(ts/fast-strip): Support type-only/uninstantiated namespaces (#9983)
Browse files Browse the repository at this point in the history
**Related issue:**

- Closes #9977
  • Loading branch information
magic-akari authored Feb 2, 2025
1 parent e9843d8 commit a72c6fa
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .changeset/popular-spiders-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
swc_core: patch
swc_fast_ts_strip: patch
---

feat(ts/fast-strip): Support type-only/uninstantiated namespaces
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ exports[`transform in strip-only mode should throw an error when it encounters a
"code": "UnsupportedSyntax",
"message": " x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | module foo {}
: ^^^^^^^^^^^^^
1 | module foo { export const m = 1; }
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\`----
",
}
Expand All @@ -134,8 +134,8 @@ exports[`transform in strip-only mode should throw an error when it encounters a
"code": "UnsupportedSyntax",
"message": " x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | namespace Foo {}
: ^^^^^^^^^^^^^^^^
1 | namespace Foo { export const m = 1; }
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\`----
",
}
Expand Down
62 changes: 32 additions & 30 deletions bindings/binding_typescript_wasm/__tests__/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,32 @@ describe("transform", () => {
export const foo: number = 1;
type Foo = number;
`,
{}
{},
);
expect(code).toMatchSnapshot();
});

describe("in strip-only mode", () => {
it("should remove declare enum", async () => {
await expect(
swc.transform(`declare enum Foo {}`, {})
swc.transform(`declare enum Foo {}`, {}),
).resolves.toMatchSnapshot();
await expect(
swc.transform(
`declare enum Foo {
A
}`,
{}
)
{},
),
).resolves.toMatchSnapshot();
expect(
swc.transform(
`declare enum Foo {
a = 2,
b,
}`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -49,8 +49,8 @@ describe("transform", () => {
type Foo = number;
type Bar = string;
const bar: Bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -59,8 +59,8 @@ describe("transform", () => {
swc.transform(
`const foo = 1;
const bar: Bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -69,8 +69,8 @@ describe("transform", () => {
swc.transform(
`const foo = 1 as number;
const bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -79,8 +79,8 @@ describe("transform", () => {
swc.transform(
`const foo = 1!;
const bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -89,8 +89,8 @@ describe("transform", () => {
swc.transform(
`const foo = 1 satisfies number;
const bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

Expand All @@ -102,59 +102,61 @@ describe("transform", () => {
bar: "bar" as any as number,
} satisfies number;
const bar = "bar";`,
{}
)
{},
),
).resolves.toMatchSnapshot();
});

it("should throw an error when it encounters an enum", async () => {
await expect(
swc.transform("enum Foo {}", {
mode: "strip-only",
})
}),
).rejects.toMatchSnapshot();
});

it("should throw an error when it encounters a namespace", async () => {
await expect(
swc.transform("namespace Foo {}", {
swc.transform("namespace Foo { export const m = 1; }", {
mode: "strip-only",
})
}),
).rejects.toMatchSnapshot();
});

it("should throw an error when it encounters a module", async () => {
await expect(
swc.transform("module foo {}", {
swc.transform("module foo { export const m = 1; }", {
mode: "strip-only",
})
}),
).rejects.toMatchSnapshot();
});

it("should not emit 'Caused by: failed to parse'", async () => {
await expect(
swc.transform("function foo() { await Promise.resolve(1); }", {
mode: "strip-only",
})
}),
).rejects.toMatchSnapshot();
});


it("should report correct error for syntax error", async () => {
await expect(
swc.transform("function foo() { invalid syntax }", {
mode: "strip-only",
})
}),
).rejects.toMatchSnapshot();
});

it("should report correct error for unsupported syntax", async () => {
await expect(
swc.transform(`enum Foo {
swc.transform(
`enum Foo {
a, b
}`, {
mode: "strip-only",
})
}`,
{
mode: "strip-only",
},
),
).rejects.toMatchSnapshot();
});
});
Expand Down
79 changes: 67 additions & 12 deletions crates/swc_fast_ts_strip/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ use swc_ecma_ast::{
ArrayPat, ArrowExpr, AutoAccessor, BindingIdent, Class, ClassDecl, ClassMethod, ClassProp,
Constructor, Decl, DefaultDecl, DoWhileStmt, EsVersion, ExportAll, ExportDecl,
ExportDefaultDecl, ExportSpecifier, FnDecl, ForInStmt, ForOfStmt, ForStmt, GetterProp, IfStmt,
ImportDecl, ImportSpecifier, NamedExport, ObjectPat, Param, Pat, PrivateMethod, PrivateProp,
Program, ReturnStmt, SetterProp, Stmt, ThrowStmt, TsAsExpr, TsConstAssertion, TsEnumDecl,
TsExportAssignment, TsImportEqualsDecl, TsIndexSignature, TsInstantiation, TsModuleDecl,
TsModuleName, TsNamespaceDecl, TsNonNullExpr, TsParamPropParam, TsSatisfiesExpr,
TsTypeAliasDecl, TsTypeAnn, TsTypeAssertion, TsTypeParamDecl, TsTypeParamInstantiation,
VarDeclarator, WhileStmt, YieldExpr,
ImportDecl, ImportSpecifier, ModuleDecl, ModuleItem, NamedExport, ObjectPat, Param, Pat,
PrivateMethod, PrivateProp, Program, ReturnStmt, SetterProp, Stmt, ThrowStmt, TsAsExpr,
TsConstAssertion, TsEnumDecl, TsExportAssignment, TsImportEqualsDecl, TsIndexSignature,
TsInstantiation, TsModuleDecl, TsModuleName, TsNamespaceBody, TsNamespaceDecl, TsNonNullExpr,
TsParamPropParam, TsSatisfiesExpr, TsTypeAliasDecl, TsTypeAnn, TsTypeAssertion,
TsTypeParamDecl, TsTypeParamInstantiation, VarDeclarator, WhileStmt, YieldExpr,
};
use swc_ecma_parser::{
lexer::Lexer,
Expand Down Expand Up @@ -1034,7 +1034,7 @@ impl Visit for TsStrip {
}

fn visit_export_decl(&mut self, n: &ExportDecl) {
if n.decl.is_ts_declare() {
if n.decl.is_ts_declare() || n.decl.is_uninstantiated() {
self.add_replacement(n.span);
self.fix_asi(n.span);
return;
Expand All @@ -1044,7 +1044,7 @@ impl Visit for TsStrip {
}

fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
if n.decl.is_ts_declare() {
if n.decl.is_ts_declare() || n.decl.is_uninstantiated() {
self.add_replacement(n.span);
self.fix_asi(n.span);
return;
Expand All @@ -1054,7 +1054,7 @@ impl Visit for TsStrip {
}

fn visit_decl(&mut self, n: &Decl) {
if n.is_ts_declare() {
if n.is_ts_declare() || n.is_uninstantiated() {
self.add_replacement(n.span());
self.fix_asi(n.span());
return;
Expand Down Expand Up @@ -1393,7 +1393,7 @@ trait IsTsDecl {
impl IsTsDecl for Decl {
fn is_ts_declare(&self) -> bool {
match self {
Self::TsInterface { .. } | Self::TsTypeAlias(..) => true,
Self::TsInterface(..) | Self::TsTypeAlias(..) => true,

Self::TsModule(module) => module.declare || matches!(module.id, TsModuleName::Str(..)),
Self::TsEnum(ref r#enum) => r#enum.declare,
Expand All @@ -1419,12 +1419,67 @@ impl IsTsDecl for DefaultDecl {
fn is_ts_declare(&self) -> bool {
match self {
Self::Class(..) => false,
DefaultDecl::Fn(r#fn) => r#fn.function.body.is_none(),
DefaultDecl::TsInterfaceDecl(..) => true,
Self::Fn(r#fn) => r#fn.function.body.is_none(),
Self::TsInterfaceDecl(..) => true,
}
}
}

trait IsUninstantiated {
fn is_uninstantiated(&self) -> bool;
}

impl IsUninstantiated for TsNamespaceBody {
fn is_uninstantiated(&self) -> bool {
match self {
Self::TsModuleBlock(block) => {
block.body.iter().all(IsUninstantiated::is_uninstantiated)
}
Self::TsNamespaceDecl(decl) => decl.body.is_uninstantiated(),
}
}
}

impl IsUninstantiated for ModuleItem {
fn is_uninstantiated(&self) -> bool {
match self {
Self::Stmt(stmt) => stmt.is_uninstantiated(),
Self::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) => {
decl.is_uninstantiated()
}
_ => false,
}
}
}

impl IsUninstantiated for Stmt {
fn is_uninstantiated(&self) -> bool {
matches!(self, Self::Decl(decl) if decl.is_uninstantiated())
}
}

impl IsUninstantiated for TsModuleDecl {
fn is_uninstantiated(&self) -> bool {
matches!(&self.body, Some(body) if body.is_uninstantiated())
}
}

impl IsUninstantiated for Decl {
fn is_uninstantiated(&self) -> bool {
match self {
Self::TsInterface(..) | Self::TsTypeAlias(..) => true,
Self::TsModule(module) => module.is_uninstantiated(),
_ => false,
}
}
}

impl IsUninstantiated for DefaultDecl {
fn is_uninstantiated(&self) -> bool {
matches!(self, Self::TsInterfaceDecl(..))
}
}

trait U8Helper {
fn is_utf8_char_boundary(&self) -> bool;
}
Expand Down
52 changes: 52 additions & 0 deletions crates/swc_fast_ts_strip/tests/errors/issue-9977.swc-stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
x TypeScript parameter property is not supported in strip-only mode
,-[6:1]
5 | // No parameter properties
6 | constructor(public foo: string) { }
: ^^^^^^^^^^^
7 | }
`----
x TypeScript namespace declaration is not supported in strip-only mode
,-[9:1]
8 |
9 | ,-> namespace IllegalBecauseInstantiated {
10 | | export const m = 1;
11 | `-> }
`----
x TypeScript namespace declaration is not supported in strip-only mode
,-[13:1]
12 |
13 | ,-> namespace AlsoIllegalBecauseInstantiated {
14 | | class PrivateClass {
15 | |
16 | | }
17 | `-> }
`----
x TypeScript namespace declaration is not supported in strip-only mode
,-[19:1]
18 |
19 | ,-> namespace IllegalBecauseNestedInstantiated {
20 | | namespace Nested {
21 | | export const m = 1;
22 | | }
23 | `-> }
`----
x TypeScript enum is not supported in strip-only mode
,-[25:1]
24 |
25 | ,-> enum NotLegalEnum {
26 | | B = 1
27 | `-> }
`----
x TypeScript import equals declaration is not supported in strip-only mode
,-[29:1]
28 |
29 | import NoGoodAlias = NotLegalEnum.B;
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`----
x TypeScript enum is not supported in strip-only mode
,-[31:1]
30 |
31 | ,-> const enum NotLegalConstEnum {
32 | | C = 2
33 | `-> }
`----
Loading

0 comments on commit a72c6fa

Please sign in to comment.