Skip to content

Commit

Permalink
[ts] Add support for instantiation expressions (#14457)
Browse files Browse the repository at this point in the history
Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
  • Loading branch information
nicolo-ribaudo and bradzacher committed May 17, 2022
1 parent 92ffff4 commit d6ff919
Show file tree
Hide file tree
Showing 56 changed files with 981 additions and 90 deletions.
4 changes: 2 additions & 2 deletions Makefile
@@ -1,6 +1,6 @@
FLOW_COMMIT = 92bbb5e9dacb8185aa73ea343954d0434b42c40b
TEST262_COMMIT = 509363bcfd24b3476dc106eabc0ac856ed5eb51d
TYPESCRIPT_COMMIT = b53073bf66571b4c99f0a93b8f76fb06b36d8991
TYPESCRIPT_COMMIT = ce85d647ef88183c019588bcf398320ce29b625a

# Fix color output until TravisCI fixes https://github.com/travis-ci/travis-ci/issues/7967
export FORCE_COLOR = true
Expand Down Expand Up @@ -148,7 +148,7 @@ test-flow-update-allowlist:
bootstrap-typescript:
rm -rf ./build/typescript
mkdir -p ./build
git clone --single-branch --shallow-since=2021-05-01 https://github.com/microsoft/TypeScript.git ./build/typescript
git clone --single-branch --shallow-since=2022-04-01 https://github.com/microsoft/TypeScript.git ./build/typescript
cd build/typescript && git checkout -q $(TYPESCRIPT_COMMIT)

test-typescript:
Expand Down
1 change: 1 addition & 0 deletions packages/babel-generator/src/generators/expressions.ts
Expand Up @@ -94,6 +94,7 @@ export function NewExpression(
this.print(node.typeParameters, node); // TS

if (node.optional) {
// TODO: This can never happen
this.token("?.");
}
this.token("(");
Expand Down
12 changes: 12 additions & 0 deletions packages/babel-generator/src/generators/typescript.ts
Expand Up @@ -265,6 +265,10 @@ export function TSTypeQuery(this: Printer, node: t.TSTypeQuery) {
this.word("typeof");
this.space();
this.print(node.exprName);

if (node.typeParameters) {
this.print(node.typeParameters, node);
}
}

export function TSTypeLiteral(this: Printer, node: t.TSTypeLiteral) {
Expand Down Expand Up @@ -514,6 +518,14 @@ export function TSTypeAssertion(this: Printer, node: t.TSTypeAssertion) {
this.print(expression, node);
}

export function TSInstantiationExpression(
this: Printer,
node: t.TSInstantiationExpression,
) {
this.print(node.expression, node);
this.print(node.typeParameters, node);
}

export function TSEnumDeclaration(this: Printer, node: t.TSEnumDeclaration) {
const { declare, const: isConst, id, members } = node;
if (declare) {
Expand Down
14 changes: 14 additions & 0 deletions packages/babel-generator/src/node/parentheses.ts
Expand Up @@ -34,6 +34,7 @@ import {
isSwitchStatement,
isTSArrayType,
isTSAsExpression,
isTSInstantiationExpression,
isTSIntersectionType,
isTSNonNullExpression,
isTSOptionalType,
Expand Down Expand Up @@ -219,6 +220,19 @@ export function TSInferType(node: any, parent: any): boolean {
return isTSArrayType(parent) || isTSOptionalType(parent);
}

export function TSInstantiationExpression(
node: t.TSInstantiationExpression,
parent: t.Node,
) {
return (
(isCallExpression(parent) ||
isOptionalCallExpression(parent) ||
isNewExpression(parent) ||
isTSInstantiationExpression(parent)) &&
!!parent.typeParameters
);
}

export function BinaryExpression(node: any, parent: any): boolean {
// let i = (1 in []);
// for ((1 in []);;);
Expand Down
@@ -0,0 +1,7 @@
a<b>;

(a<b>)<c>;
(a<b>)<c>();
(a<b>)<c>?.();
(a?.b<c>)<d>();
new (a<b>)<c>();
@@ -0,0 +1,6 @@
a<b>;
(a<b>)<c>;
(a<b>)<c>();
(a<b>)<c>?.();
(a?.b<c>)<d>();
new (a<b>)<c>();
@@ -1,2 +1,3 @@
new C<T>();
new C<T, U>();
new C<T>;
@@ -1,2 +1,3 @@
new C<T>();
new C<T, U>();
new C<T, U>();
new C<T>();
@@ -1 +1,2 @@
let x: typeof y.z;
let a: typeof b.c<d>;
@@ -1 +1,2 @@
let x: typeof y.z;
let x: typeof y.z;
let a: typeof b.c<d>;
31 changes: 16 additions & 15 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -1842,7 +1842,22 @@ export default class ExpressionParser extends LValParser {
// argument to parseSubscripts to prevent it from consuming the
// argument list.
// https://tc39.es/ecma262/#prod-NewExpression
parseNew(node: N.Expression): N.NewExpression {
parseNew(node: N.NewExpression): N.NewExpression {
this.parseNewCallee(node);

if (this.eat(tt.parenL)) {
const args = this.parseExprList(tt.parenR);
this.toReferencedList(args);
// $FlowFixMe (parseExprList should be all non-null in this case)
node.arguments = args;
} else {
node.arguments = [];
}

return this.finishNode(node, "NewExpression");
}

parseNewCallee(node: N.NewExpression): void {
node.callee = this.parseNoCallExpr();
if (node.callee.type === "Import") {
this.raise(Errors.ImportCallNotNewExpression, { at: node.callee });
Expand All @@ -1855,20 +1870,6 @@ export default class ExpressionParser extends LValParser {
at: this.state.startLoc,
});
}

this.parseNewArguments(node);
return this.finishNode(node, "NewExpression");
}

parseNewArguments(node: N.NewExpression): void {
if (this.eat(tt.parenL)) {
const args = this.parseExprList(tt.parenR);
this.toReferencedList(args);
// $FlowFixMe (parseExprList should be all non-null in this case)
node.arguments = args;
} else {
node.arguments = [];
}
}

// Parse template expression.
Expand Down
6 changes: 3 additions & 3 deletions packages/babel-parser/src/plugins/flow/index.js
Expand Up @@ -3192,16 +3192,16 @@ export default (superClass: Class<Parser>): Class<Parser> =>
);
}

parseNewArguments(node: N.NewExpression): void {
parseNewCallee(node: N.NewExpression): void {
super.parseNewCallee(node);

let targs = null;
if (this.shouldParseTypes() && this.match(tt.lt)) {
targs = this.tryParse(() =>
this.flowParseTypeParameterInstantiationCallOrNew(),
).node;
}
node.typeArguments = targs;

super.parseNewArguments(node);
}

parseAsyncArrowWithTypeParameters(
Expand Down
149 changes: 87 additions & 62 deletions packages/babel-parser/src/plugins/typescript/index.js
Expand Up @@ -12,6 +12,8 @@ import {
tt,
type TokenType,
tokenIsTemplate,
tokenCanStartExpression,
tokenIsBinaryOperator,
} from "../../tokenizer/types";
import { types as tc } from "../../tokenizer/context";
import * as N from "../../types";
Expand Down Expand Up @@ -63,6 +65,12 @@ function assert(x: boolean): void {
}
}

function tsTokenCanStartExpression(token: TokenType) {
// tsc considers binary operators as "can start expression" tokens:
// https://github.com/microsoft/TypeScript/blob/eca1b4/src/compiler/parser.ts#L4260-L4266
return tokenCanStartExpression(token) || tokenIsBinaryOperator(token);
}

type ParsingContext =
| "EnumMembers"
| "HeritageClauseElement"
Expand Down Expand Up @@ -611,6 +619,9 @@ export default (superClass: Class<Parser>): Class<Parser> =>
} else {
node.exprName = this.tsParseEntityName();
}
if (!this.hasPrecedingLineBreak() && this.match(tt.lt)) {
node.typeParameters = this.tsParseTypeArguments();
}
return this.finishNode(node, "TSTypeQuery");
}

Expand Down Expand Up @@ -1561,7 +1572,15 @@ export default (superClass: Class<Parser>): Class<Parser> =>

const delimitedList = this.tsParseDelimitedList(
"HeritageClauseElement",
this.tsParseExpressionWithTypeArguments.bind(this),
() => {
const node: N.TsExpressionWithTypeArguments = this.startNode();
node.expression = this.tsParseEntityName();
if (this.match(tt.lt)) {
node.typeParameters = this.tsParseTypeArguments();
}

return this.finishNode(node, "TSExpressionWithTypeArguments");
},
);

if (!delimitedList.length) {
Expand All @@ -1574,16 +1593,6 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return delimitedList;
}

tsParseExpressionWithTypeArguments(): N.TsExpressionWithTypeArguments {
const node: N.TsExpressionWithTypeArguments = this.startNode();
node.expression = this.tsParseEntityName();
if (this.match(tt.lt)) {
node.typeParameters = this.tsParseTypeArguments();
}

return this.finishNode(node, "TSExpressionWithTypeArguments");
}

tsParseInterfaceDeclaration(
node: N.TsInterfaceDeclaration,
properties: { declare?: true } = {},
Expand Down Expand Up @@ -2294,48 +2303,69 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
}

const node: N.CallExpression = this.startNodeAt(startPos, startLoc);
node.callee = base;

const typeArguments = this.tsParseTypeArgumentsInExpression();
if (!typeArguments) throw this.unexpected();

if (typeArguments) {
if (isOptionalCall && !this.match(tt.parenL)) {
missingParenErrorLoc = this.state.curPosition();
this.unexpected();
}
if (isOptionalCall && !this.match(tt.parenL)) {
missingParenErrorLoc = this.state.curPosition();
throw this.unexpected();
}

if (tokenIsTemplate(this.state.type)) {
const result = this.parseTaggedTemplateExpression(
base,
startPos,
startLoc,
state,
);
result.typeParameters = typeArguments;
return result;
}

if (!noCalls && this.eat(tt.parenL)) {
const node: N.CallExpression = this.startNodeAt(startPos, startLoc);
node.callee = base;
// 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,
);

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

// Handles invalid case: `f<T>(a:b)`
this.tsCheckForInvalidTypeCasts(node.arguments);

node.typeParameters = typeArguments;
if (state.optionalChainMember) {
// $FlowIgnore
node.optional = isOptionalCall;
}

return this.finishCallExpression(node, state.optionalChainMember);
} else if (tokenIsTemplate(this.state.type)) {
const result = this.parseTaggedTemplateExpression(
base,
startPos,
startLoc,
state,
);
result.typeParameters = typeArguments;
return result;
// Handles invalid case: `f<T>(a:b)`
this.tsCheckForInvalidTypeCasts(node.arguments);

node.typeParameters = typeArguments;
if (state.optionalChainMember) {
// $FlowIgnore
node.optional = isOptionalCall;
}

return this.finishCallExpression(node, state.optionalChainMember);
}

// TODO: This doesn't exactly match what TS does when it comes to ASI.
// For example,
// a<b>
// if (0);
// is not valid TS code (https://github.com/microsoft/TypeScript/issues/48654)
// However, it should correctly parse anything that is correctly parsed by TS.
if (
tsTokenCanStartExpression(this.state.type) &&
this.state.type !== tt.parenL
) {
// Bail out. We have something like a<b>c, which is not an expression with
// type arguments but an (a < b) > c comparison.
throw this.unexpected();
}

this.unexpected();
const node: N.TsInstantiationExpression = this.startNodeAt(
startPos,
startLoc,
);
node.expression = base;
node.typeParameters = typeArguments;
return this.finishNode(node, "TSInstantiationExpression");
});

if (missingParenErrorLoc) {
Expand All @@ -2348,22 +2378,17 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return super.parseSubscript(base, startPos, startLoc, noCalls, state);
}

parseNewArguments(node: N.NewExpression): void {
// tsTryParseAndCatch is expensive, so avoid if not necessary.
// 99% certain this is `new C<T>();`. But may be `new C < T;`, which is also legal.
// Also handles `new C<<T>`
if (this.match(tt.lt) || this.match(tt.bitShiftL)) {
const typeParameters = this.tsTryParseAndCatch(() => {
const args = this.tsParseTypeArgumentsInExpression();
if (!this.match(tt.parenL)) this.unexpected();
return args;
});
if (typeParameters) {
node.typeParameters = typeParameters;
}
}
parseNewCallee(node: N.NewExpression): void {
super.parseNewCallee(node);

super.parseNewArguments(node);
const { callee } = node;
if (
callee.type === "TSInstantiationExpression" &&
!callee.extra?.parenthesized
) {
node.typeParameters = callee.typeParameters;
node.callee = callee.expression;
}
}

parseExprOp(
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-parser/src/tokenizer/types.js
Expand Up @@ -411,6 +411,10 @@ export function tokenOperatorPrecedence(token: TokenType): number {
return tokenBinops[token];
}

export function tokenIsBinaryOperator(token: TokenType): boolean {
return tokenBinops[token] !== -1;
}

export function tokenIsRightAssociative(token: TokenType): boolean {
return token === tt.exponent;
}
Expand Down

0 comments on commit d6ff919

Please sign in to comment.