Skip to content

Commit

Permalink
[ts] Add support for expr satisfies Type expressions (#14211)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Oct 26, 2022
1 parent 69bbe80 commit df733b1
Show file tree
Hide file tree
Showing 23 changed files with 425 additions and 45 deletions.
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");
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, {
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 @@ -2565,20 +2548,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

0 comments on commit df733b1

Please sign in to comment.