diff --git a/.changeset/ts-token-emission.md b/.changeset/ts-token-emission.md new file mode 100644 index 00000000..66ef1a78 --- /dev/null +++ b/.changeset/ts-token-emission.md @@ -0,0 +1,11 @@ +--- +'esrap': patch +--- + +fix: print valid TypeScript for `asserts` predicates, qualified namespaces, and computed signature keys + +These were previously emitted as invalid output: + +- `asserts` type predicates were printed in the wrong order (`asserts x is T` became `x asserts T`) +- qualified namespace/module names were mangled (`namespace A.B.C` became `namespace Anamespace Bnamespace C`) +- computed keys in method/property signatures lost their brackets (`interface I { [Symbol.iterator](): T }` became `Symbol.iterator(): T`) diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index 4aba71ae..1ae21356 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -1872,7 +1872,9 @@ export default (options = {}) => { }, TSPropertySignature(node, context) { + if (node.computed) context.write('['); context.visit(node.key); + if (node.computed) context.write(']'); if (node.optional) context.write('?'); if (node.typeAnnotation) context.visit(node.typeAnnotation); }, @@ -1985,19 +1987,14 @@ export default (options = {}) => { }, TSTypePredicate(node, context) { - if (node.parameterName) { - context.visit(node.parameterName); - } else if (node.typeAnnotation) { - context.visit(node.typeAnnotation); - } + // `asserts` precedes the parameter name; `is ` follows it. Forms: + // `x is T`, `asserts x is T`, `asserts x` + if (node.asserts) context.write('asserts '); - if (node.asserts) { - context.write(' asserts '); - } else { - context.write(' is '); - } + if (node.parameterName) context.visit(node.parameterName); if (node.typeAnnotation) { + context.write(' is '); context.visit(node.typeAnnotation.typeAnnotation); } }, @@ -2083,7 +2080,9 @@ export default (options = {}) => { }, TSMethodSignature(node, context) { + if (node.computed) context.write('['); context.visit(node.key); + if (node.computed) context.write(']'); context.write('('); sequence( @@ -2241,8 +2240,17 @@ export default (options = {}) => { context.visit(node.id); } - if (!node.body) return; - context.visit(node.body); + // a qualified name (`namespace A.B.C`) is represented as nested + // `TSModuleDeclaration`s whose body is the next name part, not a block + let body = /** @type {any} */ (node.body); + while (body && body.type === 'TSModuleDeclaration') { + context.write('.'); + context.visit(body.id); + body = body.body; + } + + if (!body) return; + context.visit(body); }, TSNonNullExpression(node, context) { diff --git a/test/samples/ts-module-declaration/expected.ts b/test/samples/ts-module-declaration/expected.ts index 7204c58d..4bc68f01 100644 --- a/test/samples/ts-module-declaration/expected.ts +++ b/test/samples/ts-module-declaration/expected.ts @@ -4,4 +4,12 @@ declare global { } } +namespace A.B.C { + export const x = 1; +} + +namespace D { + export const x = 1; +} + export {}; \ No newline at end of file diff --git a/test/samples/ts-module-declaration/expected.ts.map b/test/samples/ts-module-declaration/expected.ts.map index 21d97396..1254a31d 100644 --- a/test/samples/ts-module-declaration/expected.ts.map +++ b/test/samples/ts-module-declaration/expected.ts.map @@ -5,7 +5,7 @@ "input.js" ], "sourcesContent": [ - "declare global {\n\tnamespace App {\n\t\tinterface Error {\n\t\t\tfoo: string;\n\t\t}\n\t}\n}\n\nexport {};\n" + "declare global {\n\tnamespace App {\n\t\tinterface Error {\n\t\t\tfoo: string;\n\t\t}\n\t}\n}\n\nnamespace A.B.C {\n\texport const x = 1;\n}\n\nnamespace D {\n\texport const x = 1;\n}\n\nexport {};\n" ], - "mappings": "QAAQ,MAAM;WACH,GAAG;YACF,KAAK,GACd,GAAG,EAAE,MAAM;;;;AAKd,OAAO" + "mappings": "QAAQ,MAAM;WACH,GAAG;YACF,KAAK,GACd,GAAG,EAAE,MAAM;;;;UAKJ,CAAC,CAAC,CAAC,CAAC,CAAC;CACd,MAAM,CAAC,MAAM,AAAA,CAAC,GAAG,CAAC;;;UAGT,CAAC;CACV,MAAM,CAAC,MAAM,AAAA,CAAC,GAAG,CAAC;;;AAGnB,OAAO" } \ No newline at end of file diff --git a/test/samples/ts-module-declaration/input.ts b/test/samples/ts-module-declaration/input.ts index 0e54d256..b0eca648 100644 --- a/test/samples/ts-module-declaration/input.ts +++ b/test/samples/ts-module-declaration/input.ts @@ -6,4 +6,12 @@ declare global { } } +namespace A.B.C { + export const x = 1; +} + +namespace D { + export const x = 1; +} + export {}; diff --git a/test/samples/ts-signatures/expected.ts b/test/samples/ts-signatures/expected.ts index e8d6a6af..7a0e7908 100644 --- a/test/samples/ts-signatures/expected.ts +++ b/test/samples/ts-signatures/expected.ts @@ -5,4 +5,7 @@ interface Constructable { (name: string): string } -type Constructor = { new(): object; new(name: string, age: number): Person }; \ No newline at end of file +interface Signatures { [Symbol.iterator](): number; x(): void; [k: string]: number } + +type Constructor = { new(): object; new(name: string, age: number): Person }; +type Computed = { [Symbol.iterator](): number }; \ No newline at end of file diff --git a/test/samples/ts-signatures/expected.ts.map b/test/samples/ts-signatures/expected.ts.map index ee65d1e2..7a763e2f 100644 --- a/test/samples/ts-signatures/expected.ts.map +++ b/test/samples/ts-signatures/expected.ts.map @@ -5,7 +5,7 @@ "input.js" ], "sourcesContent": [ - "interface Constructable {\n\tnew (): any;\n\tnew (value: string): MyClass;\n\tnew (value: T): GenericClass;\n\t(name: string): string;\n}\n\ntype Constructor = {\n\tnew (): object;\n\tnew (name: string, age: number): Person;\n};\n" + "interface Constructable {\n\tnew (): any;\n\tnew (value: string): MyClass;\n\tnew (value: T): GenericClass;\n\t(name: string): string;\n}\n\ninterface Signatures {\n\t[Symbol.iterator](): number;\n\tx(): void;\n\t[k: string]: number;\n}\n\ntype Constructor = {\n\tnew (): object;\n\tnew (name: string, age: number): Person;\n};\n\ntype Computed = {\n\t[Symbol.iterator](): number;\n};\n" ], - "mappings": "UAAU,aAAa;QACd,GAAG;KACN,KAAa,EAAN,MAAM,GAAG,OAAO;KACvB,CAAC,EAAE,KAAQ,EAAD,CAAC,GAAG,YAAY,CAAC,CAAC;EAChC,IAAY,EAAN,MAAM,GAAG,MAAM;;;KAGlB,WAAW,YACP,MAAM,MACT,IAAY,EAAN,MAAM,EAAE,GAAW,EAAN,MAAM,GAAG,MAAM" + "mappings": "UAAU,aAAa;QACd,GAAG;KACN,KAAa,EAAN,MAAM,GAAG,OAAO;KACvB,CAAC,EAAE,KAAQ,EAAD,CAAC,GAAG,YAAY,CAAC,CAAC;EAChC,IAAY,EAAN,MAAM,GAAG,MAAM;;;UAGb,UAAU,IAClB,MAAM,CAAC,QAAQ,KAAK,MAAM,EAC3B,CAAC,IAAI,IAAI,GACR,CAAS,EAAN,MAAM,GAAG,MAAM;;KAGf,WAAW,YACP,MAAM,MACT,IAAY,EAAN,MAAM,EAAE,GAAW,EAAN,MAAM,GAAG,MAAM;KAGnC,QAAQ,MACX,MAAM,CAAC,QAAQ,KAAK,MAAM" } \ No newline at end of file diff --git a/test/samples/ts-signatures/input.ts b/test/samples/ts-signatures/input.ts index 3892af85..847d5fc2 100644 --- a/test/samples/ts-signatures/input.ts +++ b/test/samples/ts-signatures/input.ts @@ -5,7 +5,17 @@ interface Constructable { (name: string): string; } +interface Signatures { + [Symbol.iterator](): number; + x(): void; + [k: string]: number; +} + type Constructor = { new (): object; new (name: string, age: number): Person; }; + +type Computed = { + [Symbol.iterator](): number; +}; diff --git a/test/samples/ts-types/expected.ts b/test/samples/ts-types/expected.ts index 8643ac57..173b3c13 100644 --- a/test/samples/ts-types/expected.ts +++ b/test/samples/ts-types/expected.ts @@ -11,6 +11,13 @@ type H = this; type I = `Hello, ${keyof C}`; type J = () => this is string; +type P = (x: unknown) => asserts x is string; + +function assertsString(x): asserts x is string {} +function assertsValue(x): asserts x {} +function assertsThis(x): asserts this is Foo {} +function isString(x): x is string {} + type Bird = { legs: 2 }; type Dog = { legs: 4 }; type Wolf = { legs: 4 }; diff --git a/test/samples/ts-types/expected.ts.map b/test/samples/ts-types/expected.ts.map index 412da92c..239aa79b 100644 --- a/test/samples/ts-types/expected.ts.map +++ b/test/samples/ts-types/expected.ts.map @@ -5,7 +5,7 @@ "input.js" ], "sourcesContent": [ - "type A = [x: number, y: string];\ntype B = { a: string; b: number };\n\ntype C = 'foo' | 'bar';\ntype D = C | A | B | 'foobar';\n\ntype E = A & B;\ntype F = C & 'foobar';\n\n// TODO: commented because acorn doesn't support intrinsic but oxc does\n// type G = { [a in C]: string };\n\ntype H = this;\ntype I = `Hello, ${keyof C}`;\ntype J = () => this is string;\n\ntype Bird = { legs: 2 };\ntype Dog = { legs: 4 };\ntype Wolf = { legs: 4 };\ntype Animals = Bird | Dog | Wolf;\ntype HasFourLegs = Animal extends { legs: 4 } ? Animal : never;\ntype FourLegs = HasFourLegs;\n\ntype T = [('a' | 'b')?];\n\ntype CT = new (tpl: TemplateStringsArray, ...args: Array) => (replacements: B) => A;\n\ntype X = [...number[]];\ntype TupleWithRest = [number, ...(1 extends 2 ? string[] : number[])];\n\n// TODO: commented because acorn doesn't support intrinsic but oxc does\n// type Uppercase = intrinsic;\n" + "type A = [x: number, y: string];\ntype B = { a: string; b: number };\n\ntype C = 'foo' | 'bar';\ntype D = C | A | B | 'foobar';\n\ntype E = A & B;\ntype F = C & 'foobar';\n\n// TODO: commented because acorn doesn't support intrinsic but oxc does\n// type G = { [a in C]: string };\n\ntype H = this;\ntype I = `Hello, ${keyof C}`;\ntype J = () => this is string;\ntype P = (x: unknown) => asserts x is string;\n\nfunction assertsString(x): asserts x is string {}\nfunction assertsValue(x): asserts x {}\nfunction assertsThis(x): asserts this is Foo {}\nfunction isString(x): x is string {}\n\ntype Bird = { legs: 2 };\ntype Dog = { legs: 4 };\ntype Wolf = { legs: 4 };\ntype Animals = Bird | Dog | Wolf;\ntype HasFourLegs = Animal extends { legs: 4 } ? Animal : never;\ntype FourLegs = HasFourLegs;\n\ntype T = [('a' | 'b')?];\n\ntype CT = new (tpl: TemplateStringsArray, ...args: Array) => (replacements: B) => A;\n\ntype X = [...number[]];\ntype TupleWithRest = [number, ...(1 extends 2 ? string[] : number[])];\n\n// TODO: commented because acorn doesn't support intrinsic but oxc does\n// type Uppercase = intrinsic;\n" ], - "mappings": "KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;KACzB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;KAE1B,CAAC,GAAG,KAAK,GAAG,KAAK;KACjB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ;KAExB,CAAC,GAAG,CAAC,GAAG,CAAC;KACT,CAAC,GAAG,CAAC,GAAG,QAAQ;;;;KAKhB,CAAC,GAAG,IAAI;;KACR,CAAC,mBAAmB,CAAC;KACrB,CAAC,SAAS,IAAI,IAAI,MAAM;KAExB,IAAI,KAAK,IAAI,EAAE,CAAC;KAChB,GAAG,KAAK,IAAI,EAAE,CAAC;KACf,IAAI,KAAK,IAAI,EAAE,CAAC;KAChB,OAAO,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI;KAC3B,WAAW,CAAC,MAAM,IAAI,MAAM,WAAW,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,KAAK;KACjE,QAAQ,GAAG,WAAW,CAAC,OAAO;KAE9B,CAAC,KAAK,GAAG,GAAG,GAAG;KAEf,EAAE,QAAQ,GAAyB,EAApB,oBAAoB,KAAK,IAAI,EAAE,KAAK,CAAC,OAAO,OAAO,YAAe,EAAD,CAAC,KAAK,CAAC;KAEvF,CAAC,OAAO,MAAM;KACd,aAAa,IAAI,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,KAAK,MAAM;;;" + "mappings": "KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;KACzB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;KAE1B,CAAC,GAAG,KAAK,GAAG,KAAK;KACjB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ;KAExB,CAAC,GAAG,CAAC,GAAG,CAAC;KACT,CAAC,GAAG,CAAC,GAAG,QAAQ;;;;KAKhB,CAAC,GAAG,IAAI;;KACR,CAAC,mBAAmB,CAAC;KACrB,CAAC,SAAS,IAAI,IAAI,MAAM;KACxB,CAAC,IAAI,CAAU,EAAP,OAAO,aAAa,CAAC,IAAI,MAAM;;AAE5C,QAAQ,CAAC,aAAa,CAAC,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,CAAC,AAAA,CAAC;AACjD,QAAQ,CAAC,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,AAAA,CAAC;AACtC,QAAQ,CAAC,WAAW,CAAC,CAAC,WAAW,IAAI,IAAI,GAAG,CAAC,CAAC,AAAA,CAAC;AAC/C,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,AAAA,CAAC;;KAE/B,IAAI,KAAK,IAAI,EAAE,CAAC;KAChB,GAAG,KAAK,IAAI,EAAE,CAAC;KACf,IAAI,KAAK,IAAI,EAAE,CAAC;KAChB,OAAO,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI;KAC3B,WAAW,CAAC,MAAM,IAAI,MAAM,WAAW,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,KAAK;KACjE,QAAQ,GAAG,WAAW,CAAC,OAAO;KAE9B,CAAC,KAAK,GAAG,GAAG,GAAG;KAEf,EAAE,QAAQ,GAAyB,EAApB,oBAAoB,KAAK,IAAI,EAAE,KAAK,CAAC,OAAO,OAAO,YAAe,EAAD,CAAC,KAAK,CAAC;KAEvF,CAAC,OAAO,MAAM;KACd,aAAa,IAAI,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,KAAK,MAAM;;;" } \ No newline at end of file diff --git a/test/samples/ts-types/input.ts b/test/samples/ts-types/input.ts index 51ac958b..423cb9c4 100644 --- a/test/samples/ts-types/input.ts +++ b/test/samples/ts-types/input.ts @@ -13,6 +13,12 @@ type F = C & 'foobar'; type H = this; type I = `Hello, ${keyof C}`; type J = () => this is string; +type P = (x: unknown) => asserts x is string; + +function assertsString(x): asserts x is string {} +function assertsValue(x): asserts x {} +function assertsThis(x): asserts this is Foo {} +function isString(x): x is string {} type Bird = { legs: 2 }; type Dog = { legs: 4 };