diff --git a/packages/babel-generator/src/generators/template-literals.js b/packages/babel-generator/src/generators/template-literals.js index a49b8615ed..c9a9f9c820 100644 --- a/packages/babel-generator/src/generators/template-literals.js +++ b/packages/babel-generator/src/generators/template-literals.js @@ -1,5 +1,6 @@ export function TaggedTemplateExpression(node: Object) { this.print(node.tag, node); + this.print(node.typeParameters, node); // TS this.print(node.quasi, node); } diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/input.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/input.js new file mode 100644 index 0000000000..53bbc3e3ed --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/input.js @@ -0,0 +1 @@ +f``; diff --git a/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/output.js b/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/output.js new file mode 100644 index 0000000000..44ffed74fd --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/output.js @@ -0,0 +1 @@ +f``; \ No newline at end of file diff --git a/packages/babel-parser/src/parser/expression.js b/packages/babel-parser/src/parser/expression.js index 9cc2e7bee0..48dd82d673 100644 --- a/packages/babel-parser/src/parser/expression.js +++ b/packages/babel-parser/src/parser/expression.js @@ -563,22 +563,41 @@ export default class ExpressionParser extends LValParser { } return node; } else if (this.match(tt.backQuote)) { - const node = this.startNodeAt(startPos, startLoc); - node.tag = base; - node.quasi = this.parseTemplate(true); - if (state.optionalChainMember) { - this.raise( - startPos, - "Tagged Template Literals are not allowed in optionalChain", - ); - } - return this.finishNode(node, "TaggedTemplateExpression"); + return this.parseTaggedTemplateExpression( + startPos, + startLoc, + base, + state, + ); } else { state.stop = true; return base; } } + parseTaggedTemplateExpression( + startPos: number, + startLoc: Position, + base: N.Expression, + state: N.ParseSubscriptState, + typeArguments?: ?N.TsTypeParameterInstantiation, + ): N.TaggedTemplateExpression { + const node: N.TaggedTemplateExpression = this.startNodeAt( + startPos, + startLoc, + ); + node.tag = base; + node.quasi = this.parseTemplate(true); + if (typeArguments) node.typeParameters = typeArguments; + if (state.optionalChainMember) { + this.raise( + startPos, + "Tagged Template Literals are not allowed in optionalChain", + ); + } + return this.finishNode(node, "TaggedTemplateExpression"); + } + atPossibleAsync(base: N.Expression): boolean { return ( !this.state.containsEsc && diff --git a/packages/babel-parser/src/plugins/typescript.js b/packages/babel-parser/src/plugins/typescript.js index c1fc563479..bc24dfccd7 100644 --- a/packages/babel-parser/src/plugins/typescript.js +++ b/packages/babel-parser/src/plugins/typescript.js @@ -836,16 +836,6 @@ export default (superClass: Class): Class => return this.finishNode(node, "TSTypeAssertion"); } - tsTryParseTypeArgumentsInExpression( - eatNextToken: boolean, - ): ?N.TsTypeParameterInstantiation { - return this.tsTryParseAndCatch(() => { - const res = this.tsParseTypeArguments(); - if (eatNextToken) this.expect(tt.parenL); - return res; - }); - } - tsParseHeritageClause(): $ReadOnlyArray { return this.tsParseDelimitedList( "HeritageClauseElement", @@ -1376,38 +1366,53 @@ export default (superClass: Class): Class => return this.finishNode(nonNullExpression, "TSNonNullExpression"); } - if (!noCalls && this.isRelational("<")) { - if (this.atPossibleAsync(base)) { - // Almost certainly this is a generic async function `async () => ... - // But it might be a call with a type argument `async();` - const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction( - startPos, - startLoc, - ); - if (asyncArrowFn) { - return asyncArrowFn; + // There are number of things we are going to "maybe" parse, like type arguments on + // tagged template expressions. If any of them fail, walk it back and continue. + const result = this.tsTryParseAndCatch(() => { + if (this.isRelational("<")) { + if (!noCalls && this.atPossibleAsync(base)) { + // Almost certainly this is a generic async function `async () => ... + // But it might be a call with a type argument `async();` + const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction( + startPos, + startLoc, + ); + if (asyncArrowFn) { + return asyncArrowFn; + } + } + + const node: N.CallExpression = this.startNodeAt(startPos, startLoc); + node.callee = base; + + const typeArguments = this.tsParseTypeArguments(); + + if (typeArguments) { + if (!noCalls && this.eat(tt.parenL)) { + // possibleAsync always false here, because we would have handled it above. + // $FlowIgnore (won't be any undefined arguments) + node.arguments = this.parseCallExpressionArguments( + tt.parenR, + /* possibleAsync */ false, + ); + node.typeParameters = typeArguments; + return this.finishCallExpression(node); + } else if (this.match(tt.backQuote)) { + return this.parseTaggedTemplateExpression( + startPos, + startLoc, + base, + state, + typeArguments, + ); + } } } - const node: N.CallExpression = this.startNodeAt(startPos, startLoc); - node.callee = base; + this.unexpected(); + }); - // May be passing type arguments. But may just be the `<` operator. - // 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) - node.arguments = this.parseCallExpressionArguments( - tt.parenR, - /* possibleAsync */ false, - ); - node.typeParameters = typeArguments; - return this.finishCallExpression(node); - } - } + if (result) return result; return super.parseSubscript(base, startPos, startLoc, noCalls, state); } @@ -2127,8 +2132,8 @@ export default (superClass: Class): Class => jsxParseOpeningElementAfterName( node: N.JSXOpeningElement, ): N.JSXOpeningElement { - const typeArguments = this.tsTryParseTypeArgumentsInExpression( - /*eatNextToken*/ false, + const typeArguments = this.tsTryParseAndCatch(() => + this.tsParseTypeArguments(), ); if (typeArguments) node.typeParameters = typeArguments; return super.jsxParseOpeningElementAfterName(node); diff --git a/packages/babel-parser/src/types.js b/packages/babel-parser/src/types.js index 92b6aaba09..4369302cca 100644 --- a/packages/babel-parser/src/types.js +++ b/packages/babel-parser/src/types.js @@ -577,6 +577,7 @@ export type TaggedTemplateExpression = NodeBase & { type: "TaggedTemplateExpression", tag: Expression, quasi: TemplateLiteral, + typeParameters?: ?TypeParameterInstantiationBase, // TODO: Not in spec }; export type TemplateElement = NodeBase & { diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/input.js b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/input.js new file mode 100644 index 0000000000..b540adf24f --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/input.js @@ -0,0 +1,2 @@ +new C +`` diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/output.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/output.json new file mode 100644 index 0000000000..8721061b7f --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/output.json @@ -0,0 +1,185 @@ +{ + "type": "File", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "expression": { + "type": "NewExpression", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "callee": { + "type": "TaggedTemplateExpression", + "start": 4, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 4 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "tag": { + "type": "Identifier", + "start": 4, + "end": 5, + "loc": { + "start": { + "line": 1, + "column": 4 + }, + "end": { + "line": 1, + "column": 5 + }, + "identifierName": "C" + }, + "name": "C" + }, + "quasi": { + "type": "TemplateLiteral", + "start": 9, + "end": 11, + "loc": { + "start": { + "line": 2, + "column": 0 + }, + "end": { + "line": 2, + "column": 2 + } + }, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 10, + "end": 10, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 1 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + }, + "typeParameters": { + "type": "TSTypeParameterInstantiation", + "start": 5, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 5 + }, + "end": { + "line": 1, + "column": 8 + } + }, + "params": [ + { + "type": "TSTypeReference", + "start": 6, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "typeName": { + "type": "Identifier", + "start": 6, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 7 + }, + "identifierName": "T" + }, + "name": "T" + } + } + ] + } + }, + "arguments": [] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/input.js b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/input.js new file mode 100644 index 0000000000..53bbc3e3ed --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/input.js @@ -0,0 +1 @@ +f``; diff --git a/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/output.json b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/output.json new file mode 100644 index 0000000000..c8b549d700 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/output.json @@ -0,0 +1,169 @@ +{ + "type": "File", + "start": 0, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "expression": { + "type": "TaggedTemplateExpression", + "start": 0, + "end": 6, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 6 + } + }, + "tag": { + "type": "Identifier", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + }, + "identifierName": "f" + }, + "name": "f" + }, + "quasi": { + "type": "TemplateLiteral", + "start": 4, + "end": 6, + "loc": { + "start": { + "line": 1, + "column": 4 + }, + "end": { + "line": 1, + "column": 6 + } + }, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 5, + "end": 5, + "loc": { + "start": { + "line": 1, + "column": 5 + }, + "end": { + "line": 1, + "column": 5 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + }, + "typeParameters": { + "type": "TSTypeParameterInstantiation", + "start": 1, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "params": [ + { + "type": "TSTypeReference", + "start": 2, + "end": 3, + "loc": { + "start": { + "line": 1, + "column": 2 + }, + "end": { + "line": 1, + "column": 3 + } + }, + "typeName": { + "type": "Identifier", + "start": 2, + "end": 3, + "loc": { + "start": { + "line": 1, + "column": 2 + }, + "end": { + "line": 1, + "column": 3 + }, + "identifierName": "T" + }, + "name": "T" + } + } + ] + } + } + } + ], + "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 220529a773..cc6c1be62f 100644 --- a/packages/babel-plugin-transform-typescript/src/index.js +++ b/packages/babel-plugin-transform-typescript/src/index.js @@ -268,6 +268,10 @@ export default declare((api, { jsxPragma = "React" }) => { JSXOpeningElement(path) { path.node.typeParameters = null; }, + + TaggedTemplateExpression(path) { + path.node.typeParameters = null; + }, }, }; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/input.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/input.js new file mode 100644 index 0000000000..53bbc3e3ed --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/input.js @@ -0,0 +1 @@ +f``; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/output.js new file mode 100644 index 0000000000..5b25c53f61 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/type-arguments/tagged-template/output.js @@ -0,0 +1 @@ +f``; diff --git a/packages/babel-types/src/definitions/es2015.js b/packages/babel-types/src/definitions/es2015.js index 26c0d4a344..1147acd867 100644 --- a/packages/babel-types/src/definitions/es2015.js +++ b/packages/babel-types/src/definitions/es2015.js @@ -510,6 +510,13 @@ defineType("TaggedTemplateExpression", { quasi: { validate: assertNodeType("TemplateLiteral"), }, + typeParameters: { + validate: assertNodeType( + "TypeParameterInstantiation", + "TSTypeParameterInstantiation", + ), + optional: true, + }, }, });