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 expr satisfies Type expressions #14211

Merged
merged 7 commits into from Oct 26, 2022
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
17 changes: 13 additions & 4 deletions packages/babel-generator/src/generators/typescript.ts
Expand Up @@ -522,15 +522,24 @@ export function TSTypeAliasDeclaration(
this.token(";");
}

export function TSAsExpression(this: Printer, node: t.TSAsExpression) {
const { expression, typeAnnotation } = node;
this.print(expression, node);
function TSTypeExpression(
this: Printer,
node: t.TSAsExpression | t.TSSatisfiesExpression,
) {
const { type, expression, typeAnnotation } = node;
const forceParens = !!expression.trailingComments?.length;
this.print(expression, node, true, undefined, forceParens);
this.space();
this.word("as");
this.word(type === "TSAsExpression" ? "as" : "satisfies");
JLHwung marked this conversation as resolved.
Show resolved Hide resolved
this.space();
this.print(typeAnnotation, node);
}

export {
TSTypeExpression as TSAsExpression,
TSTypeExpression as TSSatisfiesExpression,
};

export function TSTypeAssertion(this: Printer, node: t.TSTypeAssertion) {
const { typeAnnotation, expression } = node;
this.token("<");
Expand Down
11 changes: 7 additions & 4 deletions packages/babel-generator/src/node/parentheses.ts
Expand Up @@ -49,6 +49,7 @@ import {
isVariableDeclarator,
isWhileStatement,
isYieldExpression,
isTSSatisfiesExpression,
} from "@babel/types";
import type * as t from "@babel/types";
const PRECEDENCE = {
Expand Down Expand Up @@ -225,9 +226,10 @@ export function TSAsExpression() {
return true;
}

export function TSTypeAssertion() {
return true;
}
export {
TSAsExpression as TSSatisfiesExpression,
TSAsExpression as TSTypeAssertion,
};

export function TSUnionType(node: t.TSUnionType, parent: t.Node): boolean {
return (
Expand Down Expand Up @@ -368,7 +370,8 @@ export function ConditionalExpression(
isConditionalExpression(parent, { test: node }) ||
isAwaitExpression(parent) ||
isTSTypeAssertion(parent) ||
isTSAsExpression(parent)
isTSAsExpression(parent) ||
isTSSatisfiesExpression(parent)
) {
return true;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/babel-generator/src/printer.ts
Expand Up @@ -579,6 +579,7 @@ class Printer {
// trailingCommentsLineOffset also used to check if called from printJoin
// it will be ignored if `noLineTerminator||this._noLineTerminator`
trailingCommentsLineOffset?: number,
forceParens?: boolean,
) {
if (!node) return;

Expand Down Expand Up @@ -618,7 +619,9 @@ class Printer {
this._maybeAddAuxComment(this._insideAux && !oldInAux);

let shouldPrintParens = false;
if (
if (forceParens) {
shouldPrintParens = true;
} else if (
format.retainFunctionParens &&
nodeType === "FunctionExpression" &&
node.extra &&
Expand Down
@@ -0,0 +1,11 @@
x satisfies T;
x < y satisfies boolean; // (x < y) satisfies boolean;
x === 1 satisfies number; // x === (1 satisfies number);
x satisfies any satisfies T;

(
// a
x
/* b */
) satisfies T;
x /* c */ satisfies T;
@@ -0,0 +1,9 @@
(x satisfies T);
(x < y satisfies boolean); // (x < y) satisfies boolean;
x === (1 satisfies number); // x === (1 satisfies number);
((x satisfies any) satisfies T);
((
// a
x
/* b */) satisfies T);
((x /* c */) satisfies T);
Expand Up @@ -2,6 +2,7 @@ import {
isParenthesizedExpression,
isTSAsExpression,
isTSNonNullExpression,
isTSSatisfiesExpression,
isTSTypeAssertion,
isTypeCastExpression,
} from "@babel/types";
Expand All @@ -11,6 +12,7 @@ import type { NodePath } from "@babel/traverse";

export type TransparentExprWrapper =
| t.TSAsExpression
| t.TSSatisfiesExpression
| t.TSTypeAssertion
| t.TSNonNullExpression
| t.TypeCastExpression
Expand All @@ -26,6 +28,7 @@ export function isTransparentExprWrapper(
): node is TransparentExprWrapper {
return (
isTSAsExpression(node) ||
isTSSatisfiesExpression(node) ||
isTSTypeAssertion(node) ||
isTSNonNullExpression(node) ||
isTypeCastExpression(node) ||
Expand Down
62 changes: 30 additions & 32 deletions packages/babel-parser/src/plugins/typescript/index.ts
Expand Up @@ -93,7 +93,6 @@ const TSErrors = ParseErrorEnum`typescript`({
AccesorCannotDeclareThisParameter:
"'get' and 'set' accessors cannot declare 'this' parameters.",
AccesorCannotHaveTypeParameters: "An accessor cannot have type parameters.",
CannotFindName: ({ name }: { name: string }) => `Cannot find name '${name}'.`,
ClassMethodHasDeclare: "Class methods cannot have the 'declare' modifier.",
ClassMethodHasReadonly: "Class methods cannot have the 'readonly' modifier.",
ConstInitiailizerMustBeStringOrNumericLiteralOrLiteralEnumReference:
Expand Down Expand Up @@ -721,26 +720,6 @@ export default (superClass: ClassWithMixin<typeof Parser, IJSXParserMixin>) =>
return this.finishNode(node, "TSTypeParameterDeclaration");
}

tsTryNextParseConstantContext(): N.TsTypeReference | undefined | null {
if (this.lookahead().type !== tt._const) return null;

this.next();
const typeReference = this.tsParseTypeReference();

// If the type reference has type parameters, then you are using it as a
// type and not as a const signifier. We'll *never* be able to find this
// name, since const isn't allowed as a type name. So in this instance we
// get to pretend we're the type checker.
if (typeReference.typeParameters) {
this.raise(TSErrors.CannotFindName, {
Copy link
Member

Choose a reason for hiding this comment

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

CannotFindName seems to be used only here.

at: typeReference.typeName,
name: "const",
});
}

return typeReference;
}

// Note: In TypeScript implementation we must provide `yieldContext` and `awaitContext`,
// but here it's always false, because this is only used for types.
tsFillSignature(
Expand Down Expand Up @@ -1659,8 +1638,12 @@ export default (superClass: ClassWithMixin<typeof Parser, IJSXParserMixin>) =>
}

const node = this.startNode<N.TsTypeAssertion>();
const _const = this.tsTryNextParseConstantContext();
node.typeAnnotation = _const || this.tsNextThenParseType();
node.typeAnnotation = this.tsInType(() => {
this.next(); // "<"
return this.match(tt._const)
? this.tsParseTypeReference()
: this.tsParseType();
});
this.expect(tt.gt);
node.expression = this.parseMaybeUnary();
return this.finishNode(node, "TSTypeAssertion");
Expand Down Expand Up @@ -2556,20 +2539,35 @@ export default (superClass: ClassWithMixin<typeof Parser, IJSXParserMixin>) =>
leftStartLoc: Position,
minPrec: number,
): N.Expression {
let isSatisfies: boolean;
if (
tokenOperatorPrecedence(tt._in) > minPrec &&
!this.hasPrecedingLineBreak() &&
this.isContextual(tt._as)
(this.isContextual(tt._as) ||
(isSatisfies = this.isContextual(tt._satisfies)))
) {
const node = this.startNodeAt<N.TsAsExpression>(leftStartLoc);
const node = this.startNodeAt<
N.TsAsExpression | N.TsSatisfiesExpression
>(leftStartLoc);
node.expression = left;
const _const = this.tsTryNextParseConstantContext();
if (_const) {
node.typeAnnotation = _const;
} else {
node.typeAnnotation = this.tsNextThenParseType();
}
this.finishNode(node, "TSAsExpression");
node.typeAnnotation = this.tsInType(() => {
this.next(); // "as" or "satisfies"
if (this.match(tt._const)) {
if (isSatisfies) {
this.raise(Errors.UnexpectedKeyword, {
at: this.state.startLoc,
keyword: "const",
});
}
return this.tsParseTypeReference();
}

return this.tsParseType();
});
this.finishNode(
node,
isSatisfies ? "TSSatisfiesExpression" : "TSAsExpression",
);
// rescan `<`, `>` because they were scanned when this.state.inType was true
this.reScan_lt_gt();
return this.parseExprOp(
Expand Down
1 change: 1 addition & 0 deletions packages/babel-parser/src/tokenizer/types.ts
Expand Up @@ -308,6 +308,7 @@ export const tt: InternalTokenTypes = {
_mixins: createKeywordLike("mixins", { startsExpr }),
_proto: createKeywordLike("proto", { startsExpr }),
_require: createKeywordLike("require", { startsExpr }),
_satisfies: createKeywordLike("satisfies", { startsExpr }),
// start: isTSTypeOperator
_keyof: createKeywordLike("keyof", { startsExpr }),
_readonly: createKeywordLike("readonly", { startsExpr }),
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-parser/src/types.d.ts
Expand Up @@ -1639,6 +1639,10 @@ export interface TsTypeAssertion extends TsTypeAssertionLikeBase {
type: "TSTypeAssertion";
}

export type TsSatisfiesExpression = TsTypeAssertionLikeBase & {
type: "TSSatisfiesExpression";
};

export interface TsNonNullExpression extends NodeBase {
type: "TSNonNullExpression";
expression: Expression;
Expand Down
@@ -0,0 +1 @@
x satisfies const
@@ -0,0 +1,38 @@
{
"type": "File",
"start":0,"end":17,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":17,"index":17}},
"errors": [
"SyntaxError: Unexpected keyword 'const'. (1:12)"
],
"program": {
"type": "Program",
"start":0,"end":17,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":17,"index":17}},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":17,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":17,"index":17}},
"expression": {
"type": "TSSatisfiesExpression",
"start":0,"end":17,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":17,"index":17}},
"expression": {
"type": "Identifier",
"start":0,"end":1,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":1,"index":1},"identifierName":"x"},
"name": "x"
},
"typeAnnotation": {
"type": "TSTypeReference",
"start":12,"end":17,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":17,"index":17}},
"typeName": {
"type": "Identifier",
"start":12,"end":17,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":17,"index":17},"identifierName":"const"},
"name": "const"
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1,2 @@
x satisfies T;
x < y satisfies boolean; // (x < y) satisfies boolean;
@@ -0,0 +1,76 @@
{
"type": "File",
"start":0,"end":69,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":2,"column":54,"index":69}},
"program": {
"type": "Program",
"start":0,"end":69,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":2,"column":54,"index":69}},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":14,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":14,"index":14}},
"expression": {
"type": "TSSatisfiesExpression",
"start":0,"end":13,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":13,"index":13}},
"expression": {
"type": "Identifier",
"start":0,"end":1,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":1,"index":1},"identifierName":"x"},
"name": "x"
},
"typeAnnotation": {
"type": "TSTypeReference",
"start":12,"end":13,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":13,"index":13}},
"typeName": {
"type": "Identifier",
"start":12,"end":13,"loc":{"start":{"line":1,"column":12,"index":12},"end":{"line":1,"column":13,"index":13},"identifierName":"T"},
"name": "T"
}
}
}
},
{
"type": "ExpressionStatement",
"start":15,"end":39,"loc":{"start":{"line":2,"column":0,"index":15},"end":{"line":2,"column":24,"index":39}},
"expression": {
"type": "TSSatisfiesExpression",
"start":15,"end":38,"loc":{"start":{"line":2,"column":0,"index":15},"end":{"line":2,"column":23,"index":38}},
"expression": {
"type": "BinaryExpression",
"start":15,"end":20,"loc":{"start":{"line":2,"column":0,"index":15},"end":{"line":2,"column":5,"index":20}},
"left": {
"type": "Identifier",
"start":15,"end":16,"loc":{"start":{"line":2,"column":0,"index":15},"end":{"line":2,"column":1,"index":16},"identifierName":"x"},
"name": "x"
},
"operator": "<",
"right": {
"type": "Identifier",
"start":19,"end":20,"loc":{"start":{"line":2,"column":4,"index":19},"end":{"line":2,"column":5,"index":20},"identifierName":"y"},
"name": "y"
}
},
"typeAnnotation": {
"type": "TSBooleanKeyword",
"start":31,"end":38,"loc":{"start":{"line":2,"column":16,"index":31},"end":{"line":2,"column":23,"index":38}}
}
},
"trailingComments": [
{
"type": "CommentLine",
"value": " (x < y) satisfies boolean;",
"start":40,"end":69,"loc":{"start":{"line":2,"column":25,"index":40},"end":{"line":2,"column":54,"index":69}}
}
]
}
],
"directives": []
},
"comments": [
{
"type": "CommentLine",
"value": " (x < y) satisfies boolean;",
"start":40,"end":69,"loc":{"start":{"line":2,"column":25,"index":40},"end":{"line":2,"column":54,"index":69}}
}
]
}
7 changes: 5 additions & 2 deletions packages/babel-plugin-transform-typescript/src/index.ts
Expand Up @@ -593,11 +593,14 @@ export default declare((api, opts: Options) => {
path.replaceWith(path.node.expression);
},

TSAsExpression(path) {
[`TSAsExpression${
// Added in Babel 7.20.0
t.tsSatisfiesExpression ? "|TSSatisfiesExpression" : ""
}`](path: NodePath<t.TSAsExpression | t.TSSatisfiesExpression>) {
let { node }: { node: t.Expression } = path;
do {
node = node.expression;
} while (t.isTSAsExpression(node));
} while (t.isTSAsExpression(node) || t.isTSSatisfiesExpression?.(node));
path.replaceWith(node);
},

Expand Down
3 changes: 3 additions & 0 deletions packages/babel-traverse/src/path/generated/asserts.d.ts
Expand Up @@ -577,6 +577,9 @@ export interface NodePathAssetions {
opts?: object,
): asserts this is NodePath<t.TSQualifiedName>;
assertTSRestType(opts?: object): asserts this is NodePath<t.TSRestType>;
assertTSSatisfiesExpression(
opts?: object,
): asserts this is NodePath<t.TSSatisfiesExpression>;
assertTSStringKeyword(
opts?: object,
): asserts this is NodePath<t.TSStringKeyword>;
Expand Down