From 301db1b921883cedaee71845c4643cf24bde697f Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 10 Jul 2018 15:19:42 -0700 Subject: [PATCH] TypeScript: Support type arguments on JSX opening and self-closing tags (#7799) --- .../babel-generator/src/generators/jsx.js | 1 + .../typescript/type-arguments-tsx/input.js | 2 + .../type-arguments-tsx/options.json | 4 + .../typescript/type-arguments-tsx/output.js | 2 + packages/babel-parser/src/parser/index.js | 7 +- .../babel-parser/src/plugins/jsx/index.js | 11 +- .../babel-parser/src/plugins/typescript.js | 51 +++- packages/babel-parser/src/tokenizer/index.js | 2 +- packages/babel-parser/src/types.js | 10 +- .../typescript/type-arguments/tsx/input.js | 2 + .../type-arguments/tsx/options.json | 3 + .../typescript/type-arguments/tsx/output.json | 259 ++++++++++++++++++ .../src/index.js | 4 + .../test/fixtures/type-arguments/tsx/input.js | 2 + .../fixtures/type-arguments/tsx/options.json | 3 + .../fixtures/type-arguments/tsx/output.js | 2 + packages/babel-types/src/definitions/jsx.js | 7 + 17 files changed, 356 insertions(+), 16 deletions(-) create mode 100644 packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js create mode 100644 packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json create mode 100644 packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js create mode 100644 packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js create mode 100644 packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json create mode 100644 packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json create mode 100644 packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js create mode 100644 packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json create mode 100644 packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js diff --git a/packages/babel-generator/src/generators/jsx.js b/packages/babel-generator/src/generators/jsx.js index 24865630c0..89312add92 100644 --- a/packages/babel-generator/src/generators/jsx.js +++ b/packages/babel-generator/src/generators/jsx.js @@ -73,6 +73,7 @@ function spaceSeparator() { export function JSXOpeningElement(node: Object) { this.token("<"); this.print(node.name, node); + this.print(node.typeParameters, node); // TS if (node.attributes.length > 0) { this.space(); this.printJoin(node.attributes, node, { separator: spaceSeparator }); diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js new file mode 100644 index 0000000000..520e35ee90 --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/input.js @@ -0,0 +1,2 @@ +>; +/>; diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json new file mode 100644 index 0000000000..2993d04859 --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["jsx", "typescript"], + "sourceType": "module" +} \ No newline at end of file diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js new file mode 100644 index 0000000000..535f6a651d --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tsx/output.js @@ -0,0 +1,2 @@ +>; + />; \ No newline at end of file diff --git a/packages/babel-parser/src/parser/index.js b/packages/babel-parser/src/parser/index.js index 8042410ff6..6d6d9cfd8b 100644 --- a/packages/babel-parser/src/parser/index.js +++ b/packages/babel-parser/src/parser/index.js @@ -1,7 +1,7 @@ // @flow import type { Options } from "../options"; -import type { File } from "../types"; +import type { File, JSXOpeningElement } from "../types"; import type { PluginList } from "../plugin-utils"; import { getOptions } from "../options"; import StatementParser from "./statement"; @@ -11,6 +11,11 @@ export type PluginsMap = { }; export default class Parser extends StatementParser { + // Forward-declaration so typescript plugin can override jsx plugin + +jsxParseOpeningElementAfterName: ( + node: JSXOpeningElement, + ) => JSXOpeningElement; + constructor(options: ?Options, input: string) { options = getOptions(options); super(options, input); diff --git a/packages/babel-parser/src/plugins/jsx/index.js b/packages/babel-parser/src/plugins/jsx/index.js index 6ac5204377..0d32ea98d1 100644 --- a/packages/babel-parser/src/plugins/jsx/index.js +++ b/packages/babel-parser/src/plugins/jsx/index.js @@ -358,11 +358,18 @@ export default (superClass: Class): Class => this.expect(tt.jsxTagEnd); return this.finishNode(node, "JSXOpeningFragment"); } - node.attributes = []; node.name = this.jsxParseElementName(); + return this.jsxParseOpeningElementAfterName(node); + } + + jsxParseOpeningElementAfterName( + node: N.JSXOpeningElement, + ): N.JSXOpeningElement { + const attributes: N.JSXAttribute[] = []; while (!this.match(tt.slash) && !this.match(tt.jsxTagEnd)) { - node.attributes.push(this.jsxParseAttribute()); + attributes.push(this.jsxParseAttribute()); } + node.attributes = attributes; node.selfClosing = this.eat(tt.slash); this.expect(tt.jsxTagEnd); return this.finishNode(node, "JSXOpeningElement"); diff --git a/packages/babel-parser/src/plugins/typescript.js b/packages/babel-parser/src/plugins/typescript.js index e9abe7276f..c1fc563479 100644 --- a/packages/babel-parser/src/plugins/typescript.js +++ b/packages/babel-parser/src/plugins/typescript.js @@ -836,10 +836,12 @@ export default (superClass: Class): Class => return this.finishNode(node, "TSTypeAssertion"); } - tsTryParseTypeArgumentsInExpression(): ?N.TsTypeParameterInstantiation { + tsTryParseTypeArgumentsInExpression( + eatNextToken: boolean, + ): ?N.TsTypeParameterInstantiation { return this.tsTryParseAndCatch(() => { const res = this.tsParseTypeArguments(); - this.expect(tt.parenL); + if (eatNextToken) this.expect(tt.parenL); return res; }); } @@ -887,6 +889,16 @@ export default (superClass: Class): Class => return this.finishNode(node, "TSTypeAliasDeclaration"); } + tsInNoContext(cb: () => T): T { + const oldContext = this.state.context; + this.state.context = [oldContext[0]]; + try { + return cb(); + } finally { + this.state.context = oldContext; + } + } + /** * Runs `cb` in a type context. * This should be called one token *before* the first type token, @@ -1241,13 +1253,19 @@ export default (superClass: Class): Class => tsParseTypeArguments(): N.TsTypeParameterInstantiation { const node = this.startNode(); - node.params = this.tsInType(() => { - this.expectRelational("<"); - return this.tsParseDelimitedList( - "TypeParametersOrArguments", - this.tsParseType.bind(this), - ); - }); + node.params = this.tsInType(() => + // Temporarily remove a JSX parsing context, which makes us scan different tokens. + this.tsInNoContext(() => { + this.expectRelational("<"); + return this.tsParseDelimitedList( + "TypeParametersOrArguments", + this.tsParseType.bind(this), + ); + }), + ); + // This reads the next token after the `>` too, so do this in the enclosing context. + // But be sure not to parse a regex in the jsx expression ` />`, so set exprAllowed = false + this.state.exprAllowed = false; this.expectRelational(">"); return this.finishNode(node, "TSTypeParameterInstantiation"); } @@ -1375,7 +1393,10 @@ export default (superClass: Class): Class => node.callee = base; // May be passing type arguments. But may just be the `<` operator. - const typeArguments = this.tsTryParseTypeArgumentsInExpression(); // Also eats the "(" + // Note: With `/*eatNextToken*/ true` this also eats the `(` following the type arguments + const typeArguments = this.tsTryParseTypeArgumentsInExpression( + /*eatNextToken*/ true, + ); if (typeArguments) { // possibleAsync always false here, because we would have handled it above. // $FlowIgnore (won't be any undefined arguments) @@ -2102,4 +2123,14 @@ export default (superClass: Class): Class => // Avoid unnecessary lookahead in checking for abstract class unless needed! return super.canHaveLeadingDecorator() || this.isAbstractClass(); } + + jsxParseOpeningElementAfterName( + node: N.JSXOpeningElement, + ): N.JSXOpeningElement { + const typeArguments = this.tsTryParseTypeArgumentsInExpression( + /*eatNextToken*/ false, + ); + if (typeArguments) node.typeParameters = typeArguments; + return super.jsxParseOpeningElementAfterName(node); + } }; diff --git a/packages/babel-parser/src/tokenizer/index.js b/packages/babel-parser/src/tokenizer/index.js index a5655cadb4..2043832b1c 100644 --- a/packages/babel-parser/src/tokenizer/index.js +++ b/packages/babel-parser/src/tokenizer/index.js @@ -423,7 +423,7 @@ export default class Tokenizer extends LocationParser { readToken_slash(): void { // '/' - if (this.state.exprAllowed) { + if (this.state.exprAllowed && !this.state.inType) { ++this.state.pos; this.readRegexp(); return; diff --git a/packages/babel-parser/src/types.js b/packages/babel-parser/src/types.js index 0a7d2d0f52..92b6aaba09 100644 --- a/packages/babel-parser/src/types.js +++ b/packages/babel-parser/src/types.js @@ -573,7 +573,7 @@ export type TemplateLiteral = NodeBase & { expressions: $ReadOnlyArray, }; -export type TaggedTmplateExpression = NodeBase & { +export type TaggedTemplateExpression = NodeBase & { type: "TaggedTemplateExpression", tag: Expression, quasi: TemplateLiteral, @@ -820,7 +820,13 @@ export type JSXEmptyExpression = Node; export type JSXSpreadChild = Node; export type JSXExpressionContainer = Node; export type JSXAttribute = Node; -export type JSXOpeningElement = Node; +export type JSXOpeningElement = NodeBase & { + type: "JSXOpeningElement", + name: JSXNamespacedName | JSXMemberExpression, + typeParameters?: ?TypeParameterInstantiationBase, // TODO: Not in spec + attributes: $ReadOnlyArray, + selfClosing: boolean, +}; export type JSXClosingElement = Node; export type JSXElement = Node; export type JSXOpeningFragment = Node; diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js new file mode 100644 index 0000000000..520e35ee90 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/input.js @@ -0,0 +1,2 @@ +>; +/>; diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json new file mode 100644 index 0000000000..b2578f32c4 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["jsx", "typescript"] +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json new file mode 100644 index 0000000000..15f1e7ebde --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tsx/output.json @@ -0,0 +1,259 @@ +{ + "type": "File", + "start": 0, + "end": 30, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 13 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 30, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 13 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 16, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 16 + } + }, + "expression": { + "type": "JSXElement", + "start": 0, + "end": 15, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 15 + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 11 + } + }, + "name": { + "type": "JSXIdentifier", + "start": 1, + "end": 2, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 2 + } + }, + "name": "C" + }, + "typeParameters": { + "type": "TSTypeParameterInstantiation", + "start": 2, + "end": 10, + "loc": { + "start": { + "line": 1, + "column": 2 + }, + "end": { + "line": 1, + "column": 10 + } + }, + "params": [ + { + "type": "TSNumberKeyword", + "start": 3, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 3 + }, + "end": { + "line": 1, + "column": 9 + } + } + } + ] + }, + "attributes": [], + "selfClosing": false + }, + "closingElement": { + "type": "JSXClosingElement", + "start": 11, + "end": 15, + "loc": { + "start": { + "line": 1, + "column": 11 + }, + "end": { + "line": 1, + "column": 15 + } + }, + "name": { + "type": "JSXIdentifier", + "start": 13, + "end": 14, + "loc": { + "start": { + "line": 1, + "column": 13 + }, + "end": { + "line": 1, + "column": 14 + } + }, + "name": "C" + } + }, + "children": [] + } + }, + { + "type": "ExpressionStatement", + "start": 17, + "end": 30, + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 13 + } + }, + "expression": { + "type": "JSXElement", + "start": 17, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 12 + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "start": 17, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 12 + } + }, + "name": { + "type": "JSXIdentifier", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "name": "C" + }, + "typeParameters": { + "type": "TSTypeParameterInstantiation", + "start": 19, + "end": 27, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 10 + } + }, + "params": [ + { + "type": "TSNumberKeyword", + "start": 20, + "end": 26, + "loc": { + "start": { + "line": 2, + "column": 3 + }, + "end": { + "line": 2, + "column": 9 + } + } + } + ] + }, + "attributes": [], + "selfClosing": true + }, + "closingElement": null, + "children": [] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/src/index.js b/packages/babel-plugin-transform-typescript/src/index.js index f7f2b304f6..220529a773 100644 --- a/packages/babel-plugin-transform-typescript/src/index.js +++ b/packages/babel-plugin-transform-typescript/src/index.js @@ -264,6 +264,10 @@ export default declare((api, { jsxPragma = "React" }) => { NewExpression(path) { path.node.typeParameters = null; }, + + JSXOpeningElement(path) { + path.node.typeParameters = null; + }, }, }; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js new file mode 100644 index 0000000000..520e35ee90 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/input.js @@ -0,0 +1,2 @@ +>; +/>; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json new file mode 100644 index 0000000000..2c7aa7bce2 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript", { "isTSX": true }]] +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js new file mode 100644 index 0000000000..c2d492ae05 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tsx/output.js @@ -0,0 +1,2 @@ +; +; diff --git a/packages/babel-types/src/definitions/jsx.js b/packages/babel-types/src/definitions/jsx.js index c039450e95..4404273956 100644 --- a/packages/babel-types/src/definitions/jsx.js +++ b/packages/babel-types/src/definitions/jsx.js @@ -142,6 +142,13 @@ defineType("JSXOpeningElement", { assertEach(assertNodeType("JSXAttribute", "JSXSpreadAttribute")), ), }, + typeParameters: { + validate: assertNodeType( + "TypeParameterInstantiation", + "TSTypeParameterInstantiation", + ), + optional: true, + }, }, });