Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ts] Add support for instantiation expressions #14457

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will parent be an OptionalCallExpression?

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>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add

(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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has just been moved.

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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has just been moved.

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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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