From 1ba16637a1ca22be91289fc5094e6606bfb124b5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:14:11 -0400 Subject: [PATCH] Print type arguments and end expression default exports with a semicolon oxc-parser and typescript-estree v8 store type arguments on `typeArguments` (and `superTypeArguments` on classes). The printer only read `typeParameters`, so calls, `new`, type references, heritage clauses, `satisfies`, and `typeof` lost their `<...>` on print. The printer now reads `typeArguments` first and falls back to `typeParameters` for older parsers. `TSImportType` reads `source` the same way. `export default ` printed without a trailing semicolon. A following line that starts with `(` or a backtick would then parse as a call. Function and class declarations keep no semicolon. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Fvnp457DHuZC4fBa2KCiqB --- src/print.js | 47 ++++++++++++++++++++++------------- tests/parse-print-gts.test.js | 29 +++++++++++++++++++++ tests/print-file.test.js | 2 +- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/print.js b/src/print.js index e059338..b286c2a 100644 --- a/src/print.js +++ b/src/print.js @@ -170,7 +170,7 @@ export function print(node) { case "CallExpression": case "OptionalCallExpression": { const callee = print(node.callee); - const typeArgs = node.typeParameters ? print(node.typeParameters) : ""; + const typeArgs = printTypeArguments(node); const args = (node.arguments ?? []).map(print).join(", "); const opt = node.optional ? "?." : ""; return `${callee}${opt}${typeArgs}(${args})`; @@ -253,11 +253,11 @@ export function print(node) { return `await ${print(node.argument)}`; case "TaggedTemplateExpression": - return `${print(node.tag)}${print(node.quasi)}`; + return `${print(node.tag)}${printTypeArguments(node)}${print(node.quasi)}`; case "NewExpression": { const callee = print(node.callee); - const typeArgs = node.typeParameters ? print(node.typeParameters) : ""; + const typeArgs = printTypeArguments(node); const args = (node.arguments ?? []).map(print).join(", "); return `new ${callee}${typeArgs}(${args})`; } @@ -426,7 +426,8 @@ export function print(node) { const id = node.id ? ` ${print(node.id)}` : ""; const typeParams = node.typeParameters ? print(node.typeParameters) : ""; const superClass = node.superClass ? ` extends ${print(node.superClass)}` : ""; - const superTypeParams = node.superTypeParameters ? print(node.superTypeParameters) : ""; + const superTypeArgs = node.superTypeArguments ?? node.superTypeParameters; + const superTypeParams = superTypeArgs ? print(superTypeArgs) : ""; const impls = (node.implements ?? []).map(print); const implStr = impls.length ? ` implements ${impls.join(", ")}` : ""; const body = print(node.body); @@ -515,8 +516,13 @@ export function print(node) { case "ImportAttribute": return `${print(node.key)}: ${print(node.value)}`; - case "ExportDefaultDeclaration": - return `export default ${print(node.declaration)}`; + case "ExportDefaultDeclaration": { + const declaration = print(node.declaration); + // Function and class declarations end themselves. Anything else is an + // expression, and `export default x\n(y)` would otherwise parse as a call. + const semi = node.declaration?.type?.endsWith("Declaration") ? "" : ";"; + return `export default ${declaration}${semi}`; + } case "ExportNamedDeclaration": if (node.declaration) return `export ${print(node.declaration)}`; @@ -619,8 +625,7 @@ export function print(node) { case "TSTypeReference": { const name = print(node.typeName); - const params = node.typeParameters ? print(node.typeParameters) : ""; - return `${name}${params}`; + return `${name}${printTypeArguments(node)}`; } case "TSQualifiedName": @@ -789,8 +794,7 @@ export function print(node) { case "TSInterfaceHeritage": case "TSClassImplements": { const expr = print(node.expression); - const typeParams = node.typeParameters ? print(node.typeParameters) : ""; - return `${expr}${typeParams}`; + return `${expr}${printTypeArguments(node)}`; } case "TSTypeAliasDeclaration": { @@ -850,8 +854,7 @@ export function print(node) { case "TSInstantiationExpression": { const expr = print(node.expression); - const typeParams = node.typeParameters ? print(node.typeParameters) : ""; - return `${expr}${typeParams}`; + return `${expr}${printTypeArguments(node)}`; } // ── TypeScript: imports & exports ────────────────────────────── @@ -868,10 +871,9 @@ export function print(node) { return `export = ${print(node.expression)};`; case "TSImportType": { - const arg = print(node.parameter); + const arg = print(node.source ?? node.parameter ?? node.argument); const qualifier = node.qualifier ? `.${print(node.qualifier)}` : ""; - const typeParams = node.typeParameters ? print(node.typeParameters) : ""; - return `import(${arg})${qualifier}${typeParams}`; + return `import(${arg})${qualifier}${printTypeArguments(node)}`; } // ── TypeScript: parameter & type modifiers ───────────────────── @@ -893,8 +895,7 @@ export function print(node) { case "TSTypeQuery": { const name = print(node.exprName); - const typeParams = node.typeParameters ? print(node.typeParameters) : ""; - return `typeof ${name}${typeParams}`; + return `typeof ${name}${printTypeArguments(node)}`; } case "TSEmptyBodyFunctionExpression": { @@ -1044,6 +1045,18 @@ export function print(node) { } } +/** + * Prints the `<...>` type arguments of a call, `new`, tagged template, type + * reference, or heritage clause. oxc-parser and typescript-estree v8 put + * them on `typeArguments`; older typescript-estree used `typeParameters`. + * @param {object} node + * @return {string} + */ +function printTypeArguments(node) { + const args = node.typeArguments ?? node.typeParameters; + return args ? print(args) : ""; +} + /** * Prints an identifier with an optional TS type annotation. * @param {string} name diff --git a/tests/parse-print-gts.test.js b/tests/parse-print-gts.test.js index bafad31..8f046f9 100644 --- a/tests/parse-print-gts.test.js +++ b/tests/parse-print-gts.test.js @@ -22,6 +22,35 @@ describe("parse + print (.gts)", () => { } }); + it("round-trips type arguments", () => { + const sources = [ + `foo(a);`, + `new Map();`, + `let z: Array;`, + `class A extends B implements D {}`, + `interface I extends J {}`, + `let m = make;`, + `type T = typeof x;`, + `type U = import('./mod').Thing;`, + `export default x satisfies TOC;`, + ]; + for (const source of sources) { + expect(print(parse(source, { filePath: "x.gts" }).program)).toBe(source); + } + }); + + it("ends an expression `export default` with a semicolon", () => { + const sources = [ + `export default foo;`, + `export default ;`, + `export default class Foo {}`, + `export default function foo() {}`, + ]; + for (const source of sources) { + expect(print(parse(source, { filePath: "x.gts" }).program)).toBe(source); + } + }); + it("parses a gts-style file with a class component", () => { const source = ` import Component from '@glimmer/component'; diff --git a/tests/print-file.test.js b/tests/print-file.test.js index 00031b0..ea4f414 100644 --- a/tests/print-file.test.js +++ b/tests/print-file.test.js @@ -13,7 +13,7 @@ describe("print(File)", () => { it("round-trips a default-exported template", () => { const source = `export default ;`; - expect(print(toTree(source))).toBe("export default "); + expect(print(toTree(source))).toBe(source); }); it("prints an empty File", () => {