TypeScript: Support type arguments on tagged templates (#7754)

| Q                        | A
| ------------------------ | ---
| Fixed Issues?            | #7747 (partly)
| Patch: Bug Fix?          | 
| Major: Breaking Change?  | 
| Minor: New Feature?      | Yes
| Tests Added + Pass?      | Yes
| Documentation PR         |
| Any Dependency Changes?  |
| License                  | MIT

@JamesHenry This changes the AST format. CC @DanielRosenwasser for review.
Supports parsing type arguments on tagged template calls.
Should wait on Microsoft/TypeScript#23430 to be merged so we're sure we have the final syntax.
This commit is contained in:
Andy 2018-07-26 08:44:43 -07:00 committed by Brian Ng
parent db2a9fc96e
commit 8ee24fdfc0
14 changed files with 448 additions and 50 deletions

View File

@ -1,5 +1,6 @@
export function TaggedTemplateExpression(node: Object) { export function TaggedTemplateExpression(node: Object) {
this.print(node.tag, node); this.print(node.tag, node);
this.print(node.typeParameters, node); // TS
this.print(node.quasi, node); this.print(node.quasi, node);
} }

View File

@ -563,22 +563,41 @@ export default class ExpressionParser extends LValParser {
} }
return node; return node;
} else if (this.match(tt.backQuote)) { } else if (this.match(tt.backQuote)) {
const node = this.startNodeAt(startPos, startLoc); return this.parseTaggedTemplateExpression(
node.tag = base; startPos,
node.quasi = this.parseTemplate(true); startLoc,
if (state.optionalChainMember) { base,
this.raise( state,
startPos, );
"Tagged Template Literals are not allowed in optionalChain",
);
}
return this.finishNode(node, "TaggedTemplateExpression");
} else { } else {
state.stop = true; state.stop = true;
return base; 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 { atPossibleAsync(base: N.Expression): boolean {
return ( return (
!this.state.containsEsc && !this.state.containsEsc &&

View File

@ -836,16 +836,6 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return this.finishNode(node, "TSTypeAssertion"); 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<N.TsExpressionWithTypeArguments> { tsParseHeritageClause(): $ReadOnlyArray<N.TsExpressionWithTypeArguments> {
return this.tsParseDelimitedList( return this.tsParseDelimitedList(
"HeritageClauseElement", "HeritageClauseElement",
@ -1376,38 +1366,53 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return this.finishNode(nonNullExpression, "TSNonNullExpression"); return this.finishNode(nonNullExpression, "TSNonNullExpression");
} }
if (!noCalls && this.isRelational("<")) { // There are number of things we are going to "maybe" parse, like type arguments on
if (this.atPossibleAsync(base)) { // tagged template expressions. If any of them fail, walk it back and continue.
// Almost certainly this is a generic async function `async <T>() => ... const result = this.tsTryParseAndCatch(() => {
// But it might be a call with a type argument `async<T>();` if (this.isRelational("<")) {
const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction( if (!noCalls && this.atPossibleAsync(base)) {
startPos, // Almost certainly this is a generic async function `async <T>() => ...
startLoc, // But it might be a call with a type argument `async<T>();`
); const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction(
if (asyncArrowFn) { startPos,
return asyncArrowFn; 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); this.unexpected();
node.callee = base; });
// May be passing type arguments. But may just be the `<` operator. if (result) return result;
// 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);
}
}
return super.parseSubscript(base, startPos, startLoc, noCalls, state); return super.parseSubscript(base, startPos, startLoc, noCalls, state);
} }
@ -2127,8 +2132,8 @@ export default (superClass: Class<Parser>): Class<Parser> =>
jsxParseOpeningElementAfterName( jsxParseOpeningElementAfterName(
node: N.JSXOpeningElement, node: N.JSXOpeningElement,
): N.JSXOpeningElement { ): N.JSXOpeningElement {
const typeArguments = this.tsTryParseTypeArgumentsInExpression( const typeArguments = this.tsTryParseAndCatch(() =>
/*eatNextToken*/ false, this.tsParseTypeArguments(),
); );
if (typeArguments) node.typeParameters = typeArguments; if (typeArguments) node.typeParameters = typeArguments;
return super.jsxParseOpeningElementAfterName(node); return super.jsxParseOpeningElementAfterName(node);

View File

@ -577,6 +577,7 @@ export type TaggedTemplateExpression = NodeBase & {
type: "TaggedTemplateExpression", type: "TaggedTemplateExpression",
tag: Expression, tag: Expression,
quasi: TemplateLiteral, quasi: TemplateLiteral,
typeParameters?: ?TypeParameterInstantiationBase, // TODO: Not in spec
}; };
export type TemplateElement = NodeBase & { export type TemplateElement = NodeBase & {

View File

@ -0,0 +1,2 @@
new C<T>
``

View File

@ -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": []
}
}

View File

@ -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": []
}
}

View File

@ -268,6 +268,10 @@ export default declare((api, { jsxPragma = "React" }) => {
JSXOpeningElement(path) { JSXOpeningElement(path) {
path.node.typeParameters = null; path.node.typeParameters = null;
}, },
TaggedTemplateExpression(path) {
path.node.typeParameters = null;
},
}, },
}; };

View File

@ -510,6 +510,13 @@ defineType("TaggedTemplateExpression", {
quasi: { quasi: {
validate: assertNodeType("TemplateLiteral"), validate: assertNodeType("TemplateLiteral"),
}, },
typeParameters: {
validate: assertNodeType(
"TypeParameterInstantiation",
"TSTypeParameterInstantiation",
),
optional: true,
},
}, },
}); });