Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 30 additions & 17 deletions src/print.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})`;
Expand Down Expand Up @@ -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})`;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)}`;
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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 ──────────────────────────────
Expand All @@ -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 ─────────────────────
Expand All @@ -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": {
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions tests/parse-print-gts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ describe("parse + print (.gts)", () => {
}
});

it("round-trips type arguments", () => {
const sources = [
`foo<T>(a);`,
`new Map<string, number>();`,
`let z: Array<string>;`,
`class A extends B<C> implements D<E> {}`,
`interface I extends J<K> {}`,
`let m = make<T>;`,
`type T = typeof x<Y>;`,
`type U = import('./mod').Thing<V>;`,
`export default x satisfies TOC<Sig>;`,
];
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 <template>hi</template>;`,
`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';
Expand Down
2 changes: 1 addition & 1 deletion tests/print-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("print(File)", () => {

it("round-trips a default-exported template", () => {
const source = `export default <template>hi</template>;`;
expect(print(toTree(source))).toBe("export default <template>hi</template>");
expect(print(toTree(source))).toBe(source);
});

it("prints an empty File", () => {
Expand Down
Loading