diff --git a/.changeset/olive-cases-invite.md b/.changeset/olive-cases-invite.md new file mode 100644 index 00000000..3c4c6a1e --- /dev/null +++ b/.changeset/olive-cases-invite.md @@ -0,0 +1,5 @@ +--- +'esrap': patch +--- + +fix: disambiguate arrow function type parameters in JSX diff --git a/.prettierignore b/.prettierignore index a73deb65..26f99542 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ test/sandbox/_output.js expected.ts expected.jsx +expected.tsx diff --git a/src/languages/ts/index.js b/src/languages/ts/index.js index fb56022a..cb79ff96 100644 --- a/src/languages/ts/index.js +++ b/src/languages/ts/index.js @@ -207,11 +207,23 @@ function write_comment(comment, context) { } } +/** + * In a JSX file, an arrow function's `` type parameter list is parsed as a + * JSX element instead. A trailing comma (``) disambiguates it, and is only + * needed for a lone type parameter — a second parameter, a constraint or a + * default already rules JSX out. Modifiers (`const`, `in`, `out`) do not. + * @param {TSESTree.TSTypeParameterDeclaration} node + */ +function jsx_ambiguous_type_parameters(node) { + return node.params.length === 1 && !node.params[0].constraint && !node.params[0].default; +} + /** * @param {TSOptions} [options] + * @param {boolean} [jsx] set by the `tsx` language module * @returns {Visitors} */ -export default (options = {}) => { +export default (options = {}, jsx = false) => { const quote_char = options.quotes === 'double' ? '"' : "'"; const comments = options.comments ?? []; @@ -1064,7 +1076,13 @@ export default (options = {}) => { } if (node.typeParameters) { - context.visit(node.typeParameters); + if (jsx && jsx_ambiguous_type_parameters(node.typeParameters)) { + context.write('<'); + context.visit(node.typeParameters.params[0]); + context.write(',>'); + } else { + context.visit(node.typeParameters); + } } context.write('('); diff --git a/src/languages/tsx/index.js b/src/languages/tsx/index.js index eb39a143..f847c48c 100644 --- a/src/languages/tsx/index.js +++ b/src/languages/tsx/index.js @@ -8,7 +8,7 @@ import ts from '../ts/index.js'; * @returns {Visitors} */ export default (options) => ({ - ...ts(options), + ...ts(options, true), JSXElement(node, context) { context.visit(node.openingElement); diff --git a/test/esrap.test.js b/test/esrap.test.js index 5e3f9d56..59619142 100644 --- a/test/esrap.test.js +++ b/test/esrap.test.js @@ -6,6 +6,7 @@ import { expect, test } from 'vitest'; import { walk } from 'zimmerframe'; import { print } from '../src/index.js'; import { acornParse, oxcParse } from './common.js'; +import ts from '../src/languages/ts/index.js'; import tsx from '../src/languages/tsx/index.js'; import { describe } from 'node:test'; @@ -168,7 +169,7 @@ for (const dir of fs.readdirSync(`${__dirname}/samples`)) { opts = { sourceMapSource: 'input.js', sourceMapContent: input_js }; } - const { code, map } = print(ast, tsx({ comments }), opts); + const { code, map } = print(ast, (jsxMode ? tsx : ts)({ comments }), opts); const pDir = `${__dirname}/samples/${dir}/${parserName}`; if (!fs.existsSync(pDir)) fs.mkdirSync(pDir, { recursive: true }); diff --git a/test/samples/tsx-generic-arrow/expected.tsx b/test/samples/tsx-generic-arrow/expected.tsx new file mode 100644 index 00000000..62826fcf --- /dev/null +++ b/test/samples/tsx-generic-arrow/expected.tsx @@ -0,0 +1,3 @@ +const identity = (value: T): T => value; +const pair = (a: T, b: U): [T, U] => [a, b]; +const element =
{identity('x')}
; \ No newline at end of file diff --git a/test/samples/tsx-generic-arrow/expected.tsx.map b/test/samples/tsx-generic-arrow/expected.tsx.map new file mode 100644 index 00000000..1afa075d --- /dev/null +++ b/test/samples/tsx-generic-arrow/expected.tsx.map @@ -0,0 +1,11 @@ +{ + "version": 3, + "names": [], + "sources": [ + "input.js" + ], + "sourcesContent": [ + "const identity = (value: T): T => value;\nconst pair = (a: T, b: U): [T, U] => [a, b];\nconst element =
{identity('x')}
;\n" + ], + "mappings": "AAAA,MAAM,AAAA,QAAQ,IAAI,CAAC,GAAG,KAAQ,EAAD,CAAC,GAAG,CAAC,IAAI,KAAK;AAC3C,MAAM,AAAA,IAAI,IAAI,CAAC,EAAE,CAAC,EAAE,CAAI,EAAD,CAAC,EAAE,CAAI,EAAD,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;AAChD,MAAM,AAAA,OAAO,IAAI,GAAG,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG" +} \ No newline at end of file diff --git a/test/samples/tsx-generic-arrow/input.tsx b/test/samples/tsx-generic-arrow/input.tsx new file mode 100644 index 00000000..4da239b7 --- /dev/null +++ b/test/samples/tsx-generic-arrow/input.tsx @@ -0,0 +1,3 @@ +const identity = (value: T): T => value; +const pair = (a: T, b: U): [T, U] => [a, b]; +const element =
{identity('x')}
;