TypeScript: Support type arguments on JSX opening and self-closing tags (#7799)

This commit is contained in:
Andy 2018-07-10 15:19:42 -07:00 committed by Brian Ng
parent 19a1705293
commit 301db1b921
17 changed files with 356 additions and 16 deletions

View File

@ -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 });

View File

@ -0,0 +1,2 @@
<C<number>></C>;
<C<number>/>;

View File

@ -0,0 +1,4 @@
{
"plugins": ["jsx", "typescript"],
"sourceType": "module"
}

View File

@ -0,0 +1,2 @@
<C<number>></C>;
<C<number> />;

View File

@ -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);

View File

@ -358,11 +358,18 @@ export default (superClass: Class<Parser>): Class<Parser> =>
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");

View File

@ -836,10 +836,12 @@ export default (superClass: Class<Parser>): Class<Parser> =>
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<Parser>): Class<Parser> =>
return this.finishNode(node, "TSTypeAliasDeclaration");
}
tsInNoContext<T>(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<Parser>): Class<Parser> =>
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 `<C<number> />`, so set exprAllowed = false
this.state.exprAllowed = false;
this.expectRelational(">");
return this.finishNode(node, "TSTypeParameterInstantiation");
}
@ -1375,7 +1393,10 @@ export default (superClass: Class<Parser>): Class<Parser> =>
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<Parser>): Class<Parser> =>
// 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);
}
};

View File

@ -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;

View File

@ -573,7 +573,7 @@ export type TemplateLiteral = NodeBase & {
expressions: $ReadOnlyArray<Expression>,
};
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<JSXAttribute>,
selfClosing: boolean,
};
export type JSXClosingElement = Node;
export type JSXElement = Node;
export type JSXOpeningFragment = Node;

View File

@ -0,0 +1,2 @@
<C<number>></C>;
<C<number>/>;

View File

@ -0,0 +1,3 @@
{
"plugins": ["jsx", "typescript"]
}

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<C<number>></C>;
<C<number>/>;

View File

@ -0,0 +1,3 @@
{
"plugins": [["transform-typescript", { "isTSX": true }]]
}

View File

@ -0,0 +1,2 @@
<C></C>;
<C />;

View File

@ -142,6 +142,13 @@ defineType("JSXOpeningElement", {
assertEach(assertNodeType("JSXAttribute", "JSXSpreadAttribute")),
),
},
typeParameters: {
validate: assertNodeType(
"TypeParameterInstantiation",
"TSTypeParameterInstantiation",
),
optional: true,
},
},
});