feat(7481): Operator to ensure an expression is contextually typed by, 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>
This commit is contained in:
Oleksandr T
2022-08-26 10:05:52 -07:00
committed by GitHub
co-authored by Ryan Cavanaugh
parent 07157914eb
commit 164dddc48e
102 changed files with 3172 additions and 449 deletions
+18
View File
@@ -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:
@@ -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);
@@ -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:
+9 -1
View File
@@ -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
},
@@ -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
@@ -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",
+12
View File
@@ -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:
@@ -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(".");
+24
View File
@@ -222,6 +222,8 @@ namespace ts {
updateAsExpression,
createNonNullExpression,
updateNonNullExpression,
createSatisfiesExpression,
updateSatisfiesExpression,
createNonNullChain,
updateNonNullChain,
createMetaProperty,
@@ -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);
@@ -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);
}
@@ -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:
+4
View File
@@ -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;
}
+1
View File
@@ -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;
+11 -3
View File
@@ -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);
},
@@ -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);
}
@@ -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)
@@ -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 {
@@ -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);
}
+3
View File
@@ -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.
}
+1
View File
@@ -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,
+8
View File
@@ -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);
@@ -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,
+13
View File
@@ -181,6 +181,7 @@ namespace ts {
RequireKeyword,
NumberKeyword,
ObjectKeyword,
SatisfiesKeyword,
SetKeyword,
StringKeyword,
SymbolKeyword,
@@ -274,6 +275,7 @@ namespace ts {
NonNullExpression,
MetaProperty,
SyntheticExpression,
SatisfiesExpression,
// Misc
TemplateSpan,
@@ -607,6 +609,7 @@ namespace ts {
| SyntaxKind.OverrideKeyword
| SyntaxKind.RequireKeyword
| SyntaxKind.ReturnKeyword
| SyntaxKind.SatisfiesKeyword
| SyntaxKind.SetKeyword
| SyntaxKind.StaticKeyword
| SyntaxKind.StringKeyword
@@ -1007,6 +1010,7 @@ namespace ts {
| ExpressionWithTypeArguments
| AsExpression
| NonNullExpression
| SatisfiesExpression
| MetaProperty
| TemplateSpan
| Block
@@ -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
@@ -7520,6 +7530,7 @@ namespace ts {
export type OuterExpression =
| ParenthesizedExpression
| TypeAssertion
| SatisfiesExpression
| AsExpression
| NonNullExpression
| PartiallyEmittedExpression;
@@ -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
+7 -1
View File
@@ -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:
@@ -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);
}
@@ -3801,6 +3804,7 @@ namespace ts {
return OperatorPrecedence.Member;
case SyntaxKind.AsExpression:
case SyntaxKind.SatisfiesExpression:
return OperatorPrecedence.Relational;
case SyntaxKind.ThisKeyword:
@@ -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:
@@ -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;
}
+1
View File
@@ -1642,6 +1642,7 @@ namespace ts {
case SyntaxKind.OmittedExpression:
case SyntaxKind.CommaListExpression:
case SyntaxKind.PartiallyEmittedExpression:
case SyntaxKind.SatisfiesExpression:
return true;
default:
return isUnaryExpressionKind(kind);
+7 -1
View File
@@ -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,
+4
View File
@@ -1388,6 +1388,7 @@ namespace FourSlashInterface {
"package",
"readonly",
"return",
"satisfies",
"string",
"super",
"switch",
@@ -1503,6 +1504,7 @@ namespace FourSlashInterface {
"null",
"package",
"return",
"satisfies",
"super",
"switch",
"this",
@@ -1601,6 +1603,7 @@ namespace FourSlashInterface {
"package",
"readonly",
"return",
"satisfies",
"string",
"super",
"switch",
@@ -1655,6 +1658,7 @@ namespace FourSlashInterface {
"null",
"package",
"return",
"satisfies",
"super",
"switch",
"this",
+4
View File
@@ -449,6 +449,10 @@ namespace ts.CallHierarchy {
recordCallSite(node as AccessExpression);
forEachChild(node, collect);
break;
case SyntaxKind.SatisfiesExpression:
// do not descend into the type side of an assertion
collect((node as SatisfiesExpression).expression);
return;
}
if (isPartOfTypeNode(node)) {
+1
View File
@@ -369,6 +369,7 @@ namespace ts {
case SyntaxKind.InstanceOfKeyword:
case SyntaxKind.InKeyword:
case SyntaxKind.AsKeyword:
case SyntaxKind.SatisfiesKeyword:
case SyntaxKind.EqualsEqualsToken:
case SyntaxKind.ExclamationEqualsToken:
case SyntaxKind.EqualsEqualsEqualsToken:
+4
View File
@@ -2675,6 +2675,9 @@ namespace ts.Completions {
case SyntaxKind.ExtendsKeyword:
return parentKind === SyntaxKind.TypeParameter;
case SyntaxKind.SatisfiesKeyword:
return parentKind === SyntaxKind.SatisfiesExpression;
}
}
return false;
@@ -3962,6 +3965,7 @@ namespace ts.Completions {
return kind === SyntaxKind.AsyncKeyword
|| kind === SyntaxKind.AwaitKeyword
|| kind === SyntaxKind.AsKeyword
|| kind === SyntaxKind.SatisfiesKeyword
|| kind === SyntaxKind.TypeKeyword
|| !isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind);
}
+1
View File
@@ -449,6 +449,7 @@ namespace ts.formatting {
case SyntaxKind.TypePredicate:
case SyntaxKind.UnionType:
case SyntaxKind.IntersectionType:
case SyntaxKind.SatisfiesExpression:
return true;
// equals in binding elements: function foo([[x, y] = [1, 2]])
+2
View File
@@ -143,6 +143,8 @@ namespace Harness {
"exactOptionalPropertyTypes",
"useDefineForClassFields",
"useUnknownInCatchVariables",
"noUncheckedIndexedAccess",
"noPropertyAccessFromIndexSignature",
];
private fileName: string;
private justName: string;