Skip to content

Commit

Permalink
feat(7481): Operator to ensure an expression is contextually typed by…
Browse files Browse the repository at this point in the history
…, and satisfies, some type (#46827)

* feat(7481): add explicit type compatibility check with 'satisfies' expression

* Add failing test for lack of intersectioned contextual type

* Implement the behavior

* Add test corresponding to the 'if'

* Add test based on defined scenarios

* remove isExpression in favor of using type casting

* move tests from compiler to conformance folder

* update baseline

* add missing contextFlags argument

* use asserted type

* accept baseline

Co-authored-by: Ryan Cavanaugh <ryanca@microsoft.com>
  • Loading branch information
a-tarasyuk and RyanCavanaugh committed Aug 26, 2022
1 parent 0715791 commit 164dddc
Show file tree
Hide file tree
Showing 102 changed files with 3,172 additions and 449 deletions.
18 changes: 18 additions & 0 deletions src/compiler/checker.ts
Expand Up @@ -27506,6 +27506,8 @@ namespace ts {
}
case SyntaxKind.NonNullExpression:
return getContextualType(parent as NonNullExpression, contextFlags);
case SyntaxKind.SatisfiesExpression:
return getTypeFromTypeNode((parent as SatisfiesExpression).type);
case SyntaxKind.ExportAssignment:
return tryGetTypeFromEffectiveTypeNode(parent as ExportAssignment);
case SyntaxKind.JsxExpression:
Expand Down Expand Up @@ -32442,6 +32444,20 @@ namespace ts {
}
}

function checkSatisfiesExpression(node: SatisfiesExpression) {
checkSourceElement(node.type);

const targetType = getTypeFromTypeNode(node.type);
if (isErrorType(targetType)) {
return targetType;
}

const exprType = checkExpression(node.expression);
checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, node.type, node.expression, Diagnostics.Type_0_does_not_satisfy_the_expected_type_1);

return exprType;
}

function checkMetaProperty(node: MetaProperty): Type {
checkGrammarMetaProperty(node);

Expand Down Expand Up @@ -35235,6 +35251,8 @@ namespace ts {
return checkNonNullAssertion(node as NonNullExpression);
case SyntaxKind.ExpressionWithTypeArguments:
return checkExpressionWithTypeArguments(node as ExpressionWithTypeArguments);
case SyntaxKind.SatisfiesExpression:
return checkSatisfiesExpression(node as SatisfiesExpression);
case SyntaxKind.MetaProperty:
return checkMetaProperty(node as MetaProperty);
case SyntaxKind.DeleteExpression:
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/diagnosticMessages.json
Expand Up @@ -1100,7 +1100,7 @@
"category": "Error",
"code": 1359
},
"Class constructor may not be a generator.": {
"Type '{0}' does not satisfy the expected type '{1}'.": {
"category": "Error",
"code": 1360
},
Expand Down Expand Up @@ -1132,6 +1132,10 @@
"category": "Message",
"code": 1367
},
"Class constructor may not be a generator.": {
"category": "Error",
"code": 1368
},
"Did you mean '{0}'?": {
"category": "Message",
"code": 1369
Expand Down Expand Up @@ -6376,6 +6380,10 @@
"category": "Error",
"code": 8036
},
"Type satisfaction expressions can only be used in TypeScript files.": {
"category": "Error",
"code": 8037
},

"Declaration emit for this file requires using private name '{0}'. An explicit type annotation may unblock declaration emit.": {
"category": "Error",
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/emitter.ts
Expand Up @@ -1763,6 +1763,8 @@ namespace ts {
return emitNonNullExpression(node as NonNullExpression);
case SyntaxKind.ExpressionWithTypeArguments:
return emitExpressionWithTypeArguments(node as ExpressionWithTypeArguments);
case SyntaxKind.SatisfiesExpression:
return emitSatisfiesExpression(node as SatisfiesExpression);
case SyntaxKind.MetaProperty:
return emitMetaProperty(node as MetaProperty);
case SyntaxKind.SyntheticExpression:
Expand Down Expand Up @@ -2846,6 +2848,16 @@ namespace ts {
writeOperator("!");
}

function emitSatisfiesExpression(node: SatisfiesExpression) {
emitExpression(node.expression, /*parenthesizerRules*/ undefined);
if (node.type) {
writeSpace();
writeKeyword("satisfies");
writeSpace();
emit(node.type);
}
}

function emitMetaProperty(node: MetaProperty) {
writeToken(node.keywordToken, node.pos, writePunctuation);
writePunctuation(".");
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Expand Up @@ -222,6 +222,8 @@ namespace ts {
updateAsExpression,
createNonNullExpression,
updateNonNullExpression,
createSatisfiesExpression,
updateSatisfiesExpression,
createNonNullChain,
updateNonNullChain,
createMetaProperty,
Expand Down Expand Up @@ -3142,6 +3144,26 @@ namespace ts {
: node;
}

// @api
function createSatisfiesExpression(expression: Expression, type: TypeNode) {
const node = createBaseExpression<SatisfiesExpression>(SyntaxKind.SatisfiesExpression);
node.expression = expression;
node.type = type;
node.transformFlags |=
propagateChildFlags(node.expression) |
propagateChildFlags(node.type) |
TransformFlags.ContainsTypeScript;
return node;
}

// @api
function updateSatisfiesExpression(node: SatisfiesExpression, expression: Expression, type: TypeNode) {
return node.expression !== expression
|| node.type !== type
? update(createSatisfiesExpression(expression, type), node)
: node;
}

// @api
function createNonNullChain(expression: Expression) {
const node = createBaseExpression<NonNullChain>(SyntaxKind.NonNullExpression);
Expand Down Expand Up @@ -5730,6 +5752,7 @@ namespace ts {
case SyntaxKind.ParenthesizedExpression: return updateParenthesizedExpression(outerExpression, expression);
case SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression);
case SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type);
case SyntaxKind.SatisfiesExpression: return updateSatisfiesExpression(outerExpression, expression, outerExpression.type);
case SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression);
case SyntaxKind.PartiallyEmittedExpression: return updatePartiallyEmittedExpression(outerExpression, expression);
}
Expand Down Expand Up @@ -6465,6 +6488,7 @@ namespace ts {
case SyntaxKind.ArrayBindingPattern:
return TransformFlags.BindingPatternExcludes;
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.SatisfiesExpression:
case SyntaxKind.AsExpression:
case SyntaxKind.PartiallyEmittedExpression:
case SyntaxKind.ParenthesizedExpression:
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/factory/nodeTests.ts
Expand Up @@ -438,6 +438,10 @@ namespace ts {
return node.kind === SyntaxKind.AsExpression;
}

export function isSatisfiesExpression(node: Node): node is SatisfiesExpression {
return node.kind === SyntaxKind.SatisfiesExpression;
}

export function isNonNullExpression(node: Node): node is NonNullExpression {
return node.kind === SyntaxKind.NonNullExpression;
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/factory/utilities.ts
Expand Up @@ -437,6 +437,7 @@ namespace ts {
return (kinds & OuterExpressionKinds.Parentheses) !== 0;
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.AsExpression:
case SyntaxKind.SatisfiesExpression:
return (kinds & OuterExpressionKinds.TypeAssertions) !== 0;
case SyntaxKind.NonNullExpression:
return (kinds & OuterExpressionKinds.NonNullAssertions) !== 0;
Expand Down
14 changes: 11 additions & 3 deletions src/compiler/parser.ts
Expand Up @@ -393,6 +393,9 @@ namespace ts {
[SyntaxKind.NonNullExpression]: function forEachChildInNonNullExpression<T>(node: NonNullExpression, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.expression);
},
[SyntaxKind.SatisfiesExpression]: function forEachChildInSatisfiesExpression<T>(node: SatisfiesExpression, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.expression) || visitNode(cbNode, node.type);
},
[SyntaxKind.MetaProperty]: function forEachChildInMetaProperty<T>(node: MetaProperty, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.name);
},
Expand Down Expand Up @@ -841,7 +844,6 @@ namespace ts {
if (node === undefined || node.kind <= SyntaxKind.LastToken) {
return;
}

const fn = (forEachChildTable as Record<SyntaxKind, ForEachChildFunction<any>>)[node.kind];
return fn === undefined ? undefined : fn(node, cbNode, cbNodes);
}
Expand Down Expand Up @@ -5109,7 +5111,7 @@ namespace ts {
break;
}

if (token() === SyntaxKind.AsKeyword) {
if (token() === SyntaxKind.AsKeyword || token() === SyntaxKind.SatisfiesKeyword) {
// Make sure we *do* perform ASI for constructs like this:
// var x = foo
// as (Bar)
Expand All @@ -5119,8 +5121,10 @@ namespace ts {
break;
}
else {
const keywordKind = token();
nextToken();
leftOperand = makeAsExpression(leftOperand, parseType());
leftOperand = keywordKind === SyntaxKind.SatisfiesKeyword ? makeSatisfiesExpression(leftOperand, parseType()) :
makeAsExpression(leftOperand, parseType());
}
}
else {
Expand All @@ -5139,6 +5143,10 @@ namespace ts {
return getBinaryOperatorPrecedence(token()) > 0;
}

function makeSatisfiesExpression(left: Expression, right: TypeNode): SatisfiesExpression {
return finishNode(factory.createSatisfiesExpression(left, right), left.pos);
}

function makeBinaryExpression(left: Expression, operatorToken: BinaryOperatorToken, right: Expression, pos: number): BinaryExpression {
return finishNode(factory.createBinaryExpression(left, operatorToken, right), pos);
}
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/program.ts
Expand Up @@ -2372,6 +2372,9 @@ namespace ts {
case SyntaxKind.AsExpression:
diagnostics.push(createDiagnosticForNode((node as AsExpression).type, Diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files));
return "skip";
case SyntaxKind.SatisfiesExpression:
diagnostics.push(createDiagnosticForNode((node as SatisfiesExpression).type, Diagnostics.Type_satisfaction_expressions_can_only_be_used_in_TypeScript_files));
return "skip";
case SyntaxKind.TypeAssertionExpression:
Debug.fail(); // Won't parse these in a JS file anyway, as they are interpreted as JSX.
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scanner.ts
Expand Up @@ -136,6 +136,7 @@ namespace ts {
require: SyntaxKind.RequireKeyword,
global: SyntaxKind.GlobalKeyword,
return: SyntaxKind.ReturnKeyword,
satisfies: SyntaxKind.SatisfiesKeyword,
set: SyntaxKind.SetKeyword,
static: SyntaxKind.StaticKeyword,
string: SyntaxKind.StringKeyword,
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/transformers/ts.ts
Expand Up @@ -529,6 +529,9 @@ namespace ts {
// TypeScript type assertions are removed, but their subtrees are preserved.
return visitAssertionExpression(node as AssertionExpression);

case SyntaxKind.SatisfiesExpression:
return visitSatisfiesExpression(node as SatisfiesExpression);

case SyntaxKind.CallExpression:
return visitCallExpression(node as CallExpression);

Expand Down Expand Up @@ -1421,6 +1424,11 @@ namespace ts {
return factory.createPartiallyEmittedExpression(expression, node);
}

function visitSatisfiesExpression(node: SatisfiesExpression): Expression {
const expression = visitNode(node.expression, visitor, isExpression);
return factory.createPartiallyEmittedExpression(expression, node);
}

function visitCallExpression(node: CallExpression) {
return factory.updateCallExpression(
node,
Expand Down
13 changes: 13 additions & 0 deletions src/compiler/types.ts
Expand Up @@ -181,6 +181,7 @@ namespace ts {
RequireKeyword,
NumberKeyword,
ObjectKeyword,
SatisfiesKeyword,
SetKeyword,
StringKeyword,
SymbolKeyword,
Expand Down Expand Up @@ -274,6 +275,7 @@ namespace ts {
NonNullExpression,
MetaProperty,
SyntheticExpression,
SatisfiesExpression,

// Misc
TemplateSpan,
Expand Down Expand Up @@ -607,6 +609,7 @@ namespace ts {
| SyntaxKind.OverrideKeyword
| SyntaxKind.RequireKeyword
| SyntaxKind.ReturnKeyword
| SyntaxKind.SatisfiesKeyword
| SyntaxKind.SetKeyword
| SyntaxKind.StaticKeyword
| SyntaxKind.StringKeyword
Expand Down Expand Up @@ -1007,6 +1010,7 @@ namespace ts {
| ExpressionWithTypeArguments
| AsExpression
| NonNullExpression
| SatisfiesExpression
| MetaProperty
| TemplateSpan
| Block
Expand Down Expand Up @@ -2815,6 +2819,12 @@ namespace ts {
readonly expression: UnaryExpression;
}

export interface SatisfiesExpression extends Expression {
readonly kind: SyntaxKind.SatisfiesExpression;
readonly expression: Expression;
readonly type: TypeNode;
}

export type AssertionExpression =
| TypeAssertion
| AsExpression
Expand Down Expand Up @@ -7520,6 +7530,7 @@ namespace ts {
export type OuterExpression =
| ParenthesizedExpression
| TypeAssertion
| SatisfiesExpression
| AsExpression
| NonNullExpression
| PartiallyEmittedExpression;
Expand Down Expand Up @@ -7856,6 +7867,8 @@ namespace ts {
updateNonNullChain(node: NonNullChain, expression: Expression): NonNullChain;
createMetaProperty(keywordToken: MetaProperty["keywordToken"], name: Identifier): MetaProperty;
updateMetaProperty(node: MetaProperty, name: Identifier): MetaProperty;
createSatisfiesExpression(expression: Expression, type: TypeNode): SatisfiesExpression;
updateSatisfiesExpression(node: SatisfiesExpression, expression: Expression, type: TypeNode): SatisfiesExpression;

//
// Misc
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/utilities.ts
Expand Up @@ -1995,6 +1995,7 @@ namespace ts {
case SyntaxKind.TaggedTemplateExpression:
case SyntaxKind.AsExpression:
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.SatisfiesExpression:
case SyntaxKind.NonNullExpression:
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.FunctionExpression:
Expand Down Expand Up @@ -2095,6 +2096,8 @@ namespace ts {
return (parent as ExpressionWithTypeArguments).expression === node && !isPartOfTypeNode(parent);
case SyntaxKind.ShorthandPropertyAssignment:
return (parent as ShorthandPropertyAssignment).objectAssignmentInitializer === node;
case SyntaxKind.SatisfiesExpression:
return node === (parent as SatisfiesExpression).expression;
default:
return isExpressionNode(parent);
}
Expand Down Expand Up @@ -3801,6 +3804,7 @@ namespace ts {
return OperatorPrecedence.Member;

case SyntaxKind.AsExpression:
case SyntaxKind.SatisfiesExpression:
return OperatorPrecedence.Relational;

case SyntaxKind.ThisKeyword:
Expand Down Expand Up @@ -3859,6 +3863,7 @@ namespace ts {
case SyntaxKind.InstanceOfKeyword:
case SyntaxKind.InKeyword:
case SyntaxKind.AsKeyword:
case SyntaxKind.SatisfiesKeyword:
return OperatorPrecedence.Relational;
case SyntaxKind.LessThanLessThanToken:
case SyntaxKind.GreaterThanGreaterThanToken:
Expand Down Expand Up @@ -5930,7 +5935,8 @@ namespace ts {
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.NonNullExpression:
case SyntaxKind.PartiallyEmittedExpression:
node = (node as CallExpression | PropertyAccessExpression | ElementAccessExpression | AsExpression | NonNullExpression | PartiallyEmittedExpression).expression;
case SyntaxKind.SatisfiesExpression:
node = (node as CallExpression | PropertyAccessExpression | ElementAccessExpression | AsExpression | NonNullExpression | PartiallyEmittedExpression | SatisfiesExpression).expression;
continue;
}

Expand Down
1 change: 1 addition & 0 deletions src/compiler/utilitiesPublic.ts
Expand Up @@ -1642,6 +1642,7 @@ namespace ts {
case SyntaxKind.OmittedExpression:
case SyntaxKind.CommaListExpression:
case SyntaxKind.PartiallyEmittedExpression:
case SyntaxKind.SatisfiesExpression:
return true;
default:
return isUnaryExpressionKind(kind);
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/visitorPublic.ts
Expand Up @@ -888,13 +888,19 @@ namespace ts {
nodeVisitor(node.type, visitor, isTypeNode));
},

[SyntaxKind.SatisfiesExpression]: function visitEachChildOfSatisfiesExpression(node, visitor, context, _nodesVisitor, nodeVisitor, _tokenVisitor) {
return context.factory.updateSatisfiesExpression(node,
nodeVisitor(node.expression, visitor, isExpression),
nodeVisitor(node.type, visitor, isTypeNode));
},

[SyntaxKind.NonNullExpression]: function visitEachChildOfNonNullExpression(node, visitor, context, _nodesVisitor, nodeVisitor, _tokenVisitor) {
return isOptionalChain(node) ?
context.factory.updateNonNullChain(node,
nodeVisitor(node.expression, visitor, isExpression)) :
context.factory.updateNonNullExpression(node,
nodeVisitor(node.expression, visitor, isExpression));
},
},

[SyntaxKind.MetaProperty]: function visitEachChildOfMetaProperty(node, visitor, context, _nodesVisitor, nodeVisitor, _tokenVisitor) {
return context.factory.updateMetaProperty(node,
Expand Down

0 comments on commit 164dddc

Please sign in to comment.