Skip to content

Commit b58a29b

Browse files
authored
Fix emit for optional chain with non-null assertion (#36539)
* Fix emit for optional chain with non-null assertion * Treat '!' differently inside chain vs end of chain * remove dead code and fix comment
1 parent a04225d commit b58a29b

25 files changed

+288
-69
lines changed

Diff for: src/compiler/binder.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,9 @@ namespace ts {
833833
case SyntaxKind.CallExpression:
834834
bindCallExpressionFlow(<CallExpression>node);
835835
break;
836+
case SyntaxKind.NonNullExpression:
837+
bindNonNullExpressionFlow(<NonNullExpression>node);
838+
break;
836839
case SyntaxKind.JSDocTypedefTag:
837840
case SyntaxKind.JSDocCallbackTag:
838841
case SyntaxKind.JSDocEnumTag:
@@ -1668,15 +1671,17 @@ namespace ts {
16681671
}
16691672

16701673
function bindOptionalChainRest(node: OptionalChain) {
1671-
bind(node.questionDotToken);
16721674
switch (node.kind) {
16731675
case SyntaxKind.PropertyAccessExpression:
1676+
bind(node.questionDotToken);
16741677
bind(node.name);
16751678
break;
16761679
case SyntaxKind.ElementAccessExpression:
1680+
bind(node.questionDotToken);
16771681
bind(node.argumentExpression);
16781682
break;
16791683
case SyntaxKind.CallExpression:
1684+
bind(node.questionDotToken);
16801685
bindEach(node.typeArguments);
16811686
bindEach(node.arguments);
16821687
break;
@@ -1695,7 +1700,7 @@ namespace ts {
16951700
// and build it's CFA graph as if it were the first condition (`a && ...`). Then we bind the rest
16961701
// of the node as part of the "true" branch, and continue to do so as we ascend back up to the outermost
16971702
// chain node. We then treat the entire node as the right side of the expression.
1698-
const preChainLabel = node.questionDotToken ? createBranchLabel() : undefined;
1703+
const preChainLabel = isOptionalChainRoot(node) ? createBranchLabel() : undefined;
16991704
bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget);
17001705
if (preChainLabel) {
17011706
currentFlow = finishFlowLabel(preChainLabel);
@@ -1718,7 +1723,16 @@ namespace ts {
17181723
}
17191724
}
17201725

1721-
function bindAccessExpressionFlow(node: AccessExpression) {
1726+
function bindNonNullExpressionFlow(node: NonNullExpression | NonNullChain) {
1727+
if (isOptionalChain(node)) {
1728+
bindOptionalChainFlow(node);
1729+
}
1730+
else {
1731+
bindEachChild(node);
1732+
}
1733+
}
1734+
1735+
function bindAccessExpressionFlow(node: AccessExpression | PropertyAccessChain | ElementAccessChain) {
17221736
if (isOptionalChain(node)) {
17231737
bindOptionalChainFlow(node);
17241738
}
@@ -1727,7 +1741,7 @@ namespace ts {
17271741
}
17281742
}
17291743

1730-
function bindCallExpressionFlow(node: CallExpression) {
1744+
function bindCallExpressionFlow(node: CallExpression | CallChain) {
17311745
if (isOptionalChain(node)) {
17321746
bindOptionalChainFlow(node);
17331747
}

Diff for: src/compiler/checker.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -26503,8 +26503,15 @@ namespace ts {
2650326503
return targetType;
2650426504
}
2650526505

26506+
function checkNonNullChain(node: NonNullChain) {
26507+
const leftType = checkExpression(node.expression);
26508+
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
26509+
return propagateOptionalTypeMarker(getNonNullableType(nonOptionalType), node, nonOptionalType !== leftType);
26510+
}
26511+
2650626512
function checkNonNullAssertion(node: NonNullExpression) {
26507-
return getNonNullableType(checkExpression(node.expression));
26513+
return node.flags & NodeFlags.OptionalChain ? checkNonNullChain(node as NonNullChain) :
26514+
getNonNullableType(checkExpression(node.expression));
2650826515
}
2650926516

2651026517
function checkMetaProperty(node: MetaProperty): Type {

Diff for: src/compiler/debug.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ namespace ts {
166166
}
167167
}
168168

169-
export function assertNotNode<T extends Node, U extends T>(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is Exclude<T, U>;
169+
export function assertNotNode<T extends Node, U extends T>(node: T | undefined, test: (node: Node) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is Exclude<T, U>;
170170
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void;
171171
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) {
172172
if (shouldAssertFunction(AssertionLevel.Normal, "assertNotNode")) {

Diff for: src/compiler/factory.ts

+9-24
Original file line numberDiff line numberDiff line change
@@ -1335,9 +1335,11 @@ namespace ts {
13351335

13361336
export const enum OuterExpressionKinds {
13371337
Parentheses = 1 << 0,
1338-
Assertions = 1 << 1,
1339-
PartiallyEmittedExpressions = 1 << 2,
1338+
TypeAssertions = 1 << 1,
1339+
NonNullAssertions = 1 << 2,
1340+
PartiallyEmittedExpressions = 1 << 3,
13401341

1342+
Assertions = TypeAssertions | NonNullAssertions,
13411343
All = Parentheses | Assertions | PartiallyEmittedExpressions
13421344
}
13431345

@@ -1349,8 +1351,9 @@ namespace ts {
13491351
return (kinds & OuterExpressionKinds.Parentheses) !== 0;
13501352
case SyntaxKind.TypeAssertionExpression:
13511353
case SyntaxKind.AsExpression:
1354+
return (kinds & OuterExpressionKinds.TypeAssertions) !== 0;
13521355
case SyntaxKind.NonNullExpression:
1353-
return (kinds & OuterExpressionKinds.Assertions) !== 0;
1356+
return (kinds & OuterExpressionKinds.NonNullAssertions) !== 0;
13541357
case SyntaxKind.PartiallyEmittedExpression:
13551358
return (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) !== 0;
13561359
}
@@ -1360,34 +1363,16 @@ namespace ts {
13601363
export function skipOuterExpressions(node: Expression, kinds?: OuterExpressionKinds): Expression;
13611364
export function skipOuterExpressions(node: Node, kinds?: OuterExpressionKinds): Node;
13621365
export function skipOuterExpressions(node: Node, kinds = OuterExpressionKinds.All) {
1363-
let previousNode: Node;
1364-
do {
1365-
previousNode = node;
1366-
if (kinds & OuterExpressionKinds.Parentheses) {
1367-
node = skipParentheses(node);
1368-
}
1369-
1370-
if (kinds & OuterExpressionKinds.Assertions) {
1371-
node = skipAssertions(node);
1372-
}
1373-
1374-
if (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) {
1375-
node = skipPartiallyEmittedExpressions(node);
1376-
}
1366+
while (isOuterExpression(node, kinds)) {
1367+
node = node.expression;
13771368
}
1378-
while (previousNode !== node);
1379-
13801369
return node;
13811370
}
13821371

13831372
export function skipAssertions(node: Expression): Expression;
13841373
export function skipAssertions(node: Node): Node;
13851374
export function skipAssertions(node: Node): Node {
1386-
while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) {
1387-
node = (<AssertionExpression | NonNullExpression>node).expression;
1388-
}
1389-
1390-
return node;
1375+
return skipOuterExpressions(node, OuterExpressionKinds.Assertions);
13911376
}
13921377

13931378
function updateOuterExpression(outerExpression: OuterExpression, expression: Expression) {

Diff for: src/compiler/factoryPublic.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -1076,10 +1076,8 @@ namespace ts {
10761076
}
10771077

10781078
export function updatePropertyAccess(node: PropertyAccessExpression, expression: Expression, name: Identifier | PrivateIdentifier) {
1079-
if (isOptionalChain(node) && isIdentifier(node.name) && isIdentifier(name)) {
1080-
// Not sure why this cast was necessary: the previous line should already establish that node.name is an identifier
1081-
const theNode = node as (typeof node & { name: Identifier });
1082-
return updatePropertyAccessChain(theNode, expression, node.questionDotToken, name);
1079+
if (isPropertyAccessChain(node)) {
1080+
return updatePropertyAccessChain(node, expression, node.questionDotToken, cast(name, isIdentifier));
10831081
}
10841082
// Because we are updating existed propertyAccess we want to inherit its emitFlags
10851083
// instead of using the default from createPropertyAccess
@@ -1653,11 +1651,28 @@ namespace ts {
16531651
}
16541652

16551653
export function updateNonNullExpression(node: NonNullExpression, expression: Expression) {
1654+
if (isNonNullChain(node)) {
1655+
return updateNonNullChain(node, expression);
1656+
}
16561657
return node.expression !== expression
16571658
? updateNode(createNonNullExpression(expression), node)
16581659
: node;
16591660
}
16601661

1662+
export function createNonNullChain(expression: Expression) {
1663+
const node = <NonNullChain>createSynthesizedNode(SyntaxKind.NonNullExpression);
1664+
node.flags |= NodeFlags.OptionalChain;
1665+
node.expression = parenthesizeForAccess(expression);
1666+
return node;
1667+
}
1668+
1669+
export function updateNonNullChain(node: NonNullChain, expression: Expression) {
1670+
Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a NonNullExpression using updateNonNullChain. Use updateNonNullExpression instead.");
1671+
return node.expression !== expression
1672+
? updateNode(createNonNullChain(expression), node)
1673+
: node;
1674+
}
1675+
16611676
export function createMetaProperty(keywordToken: MetaProperty["keywordToken"], name: Identifier) {
16621677
const node = <MetaProperty>createSynthesizedNode(SyntaxKind.MetaProperty);
16631678
node.keywordToken = keywordToken;

Diff for: src/compiler/parser.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -4759,12 +4759,34 @@ namespace ts {
47594759
&& lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate);
47604760
}
47614761

4762+
function tryReparseOptionalChain(node: Expression) {
4763+
if (node.flags & NodeFlags.OptionalChain) {
4764+
return true;
4765+
}
4766+
// check for an optional chain in a non-null expression
4767+
if (isNonNullExpression(node)) {
4768+
let expr = node.expression;
4769+
while (isNonNullExpression(expr) && !(expr.flags & NodeFlags.OptionalChain)) {
4770+
expr = expr.expression;
4771+
}
4772+
if (expr.flags & NodeFlags.OptionalChain) {
4773+
// this is part of an optional chain. Walk down from `node` to `expression` and set the flag.
4774+
while (isNonNullExpression(node)) {
4775+
node.flags |= NodeFlags.OptionalChain;
4776+
node = node.expression;
4777+
}
4778+
return true;
4779+
}
4780+
}
4781+
return false;
4782+
}
4783+
47624784
function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) {
47634785
const propertyAccess = <PropertyAccessExpression>createNode(SyntaxKind.PropertyAccessExpression, expression.pos);
47644786
propertyAccess.expression = expression;
47654787
propertyAccess.questionDotToken = questionDotToken;
47664788
propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true);
4767-
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
4789+
if (questionDotToken || tryReparseOptionalChain(expression)) {
47684790
propertyAccess.flags |= NodeFlags.OptionalChain;
47694791
if (isPrivateIdentifier(propertyAccess.name)) {
47704792
parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers);
@@ -4790,7 +4812,7 @@ namespace ts {
47904812
}
47914813

47924814
parseExpected(SyntaxKind.CloseBracketToken);
4793-
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
4815+
if (questionDotToken || tryReparseOptionalChain(expression)) {
47944816
indexedAccess.flags |= NodeFlags.OptionalChain;
47954817
}
47964818
return finishNode(indexedAccess);
@@ -4877,7 +4899,7 @@ namespace ts {
48774899
callExpr.questionDotToken = questionDotToken;
48784900
callExpr.typeArguments = typeArguments;
48794901
callExpr.arguments = parseArgumentList();
4880-
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
4902+
if (questionDotToken || tryReparseOptionalChain(expression)) {
48814903
callExpr.flags |= NodeFlags.OptionalChain;
48824904
}
48834905
expression = finishNode(callExpr);
@@ -4889,7 +4911,7 @@ namespace ts {
48894911
callExpr.expression = expression;
48904912
callExpr.questionDotToken = questionDotToken;
48914913
callExpr.arguments = parseArgumentList();
4892-
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
4914+
if (questionDotToken || tryReparseOptionalChain(expression)) {
48934915
callExpr.flags |= NodeFlags.OptionalChain;
48944916
}
48954917
expression = finishNode(callExpr);

Diff for: src/compiler/transformers/es2020.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ namespace ts {
4242
}
4343

4444
function flattenChain(chain: OptionalChain) {
45+
Debug.assertNotNode(chain, isNonNullChain);
4546
const links: OptionalChain[] = [chain];
4647
while (!chain.questionDotToken && !isTaggedTemplateExpression(chain)) {
47-
chain = cast(chain.expression, isOptionalChain);
48+
chain = cast(skipPartiallyEmittedExpressions(chain.expression), isOptionalChain);
49+
Debug.assertNotNode(chain, isNonNullChain);
4850
links.unshift(chain);
4951
}
5052
return { expression: chain.expression, chain: links };

Diff for: src/compiler/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1935,6 +1935,7 @@ namespace ts {
19351935
| PropertyAccessChain
19361936
| ElementAccessChain
19371937
| CallChain
1938+
| NonNullChain
19381939
;
19391940

19401941
/* @internal */
@@ -2029,6 +2030,10 @@ namespace ts {
20292030
expression: Expression;
20302031
}
20312032

2033+
export interface NonNullChain extends NonNullExpression {
2034+
_optionalChainBrand: any;
2035+
}
2036+
20322037
// NOTE: MetaProperty is really a MemberExpression, but we consider it a PrimaryExpression
20332038
// for the same reasons we treat NewExpression as a PrimaryExpression.
20342039
export interface MetaProperty extends PrimaryExpression {

Diff for: src/compiler/utilities.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -2674,11 +2674,7 @@ namespace ts {
26742674
export function skipParentheses(node: Expression): Expression;
26752675
export function skipParentheses(node: Node): Node;
26762676
export function skipParentheses(node: Node): Node {
2677-
while (node.kind === SyntaxKind.ParenthesizedExpression) {
2678-
node = (node as ParenthesizedExpression).expression;
2679-
}
2680-
2681-
return node;
2677+
return skipOuterExpressions(node, OuterExpressionKinds.Parentheses);
26822678
}
26832679

26842680
function skipParenthesesUp(node: Node): Node {

Diff for: src/compiler/utilitiesPublic.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -1086,17 +1086,18 @@ namespace ts {
10861086
return isCallExpression(node) && !!(node.flags & NodeFlags.OptionalChain);
10871087
}
10881088

1089-
export function isOptionalChain(node: Node): node is PropertyAccessChain | ElementAccessChain | CallChain {
1089+
export function isOptionalChain(node: Node): node is PropertyAccessChain | ElementAccessChain | CallChain | NonNullChain {
10901090
const kind = node.kind;
10911091
return !!(node.flags & NodeFlags.OptionalChain) &&
10921092
(kind === SyntaxKind.PropertyAccessExpression
10931093
|| kind === SyntaxKind.ElementAccessExpression
1094-
|| kind === SyntaxKind.CallExpression);
1094+
|| kind === SyntaxKind.CallExpression
1095+
|| kind === SyntaxKind.NonNullExpression);
10951096
}
10961097

10971098
/* @internal */
10981099
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
1099-
return isOptionalChain(node) && !!node.questionDotToken;
1100+
return isOptionalChain(node) && !isNonNullExpression(node) && !!node.questionDotToken;
11001101
}
11011102

11021103
/**
@@ -1111,17 +1112,18 @@ namespace ts {
11111112
* Determines whether a node is the outermost `OptionalChain` in an ECMAScript `OptionalExpression`:
11121113
*
11131114
* 1. For `a?.b.c`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.`)
1114-
* 2. For `(a?.b.c).d`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.` since parens end the chain)
1115-
* 3. For `a?.b.c?.d`, both `a?.b.c` and `a?.b.c?.d` are outermost (`c` is the end of the chain starting at `a?.`, and `d` is
1115+
* 2. For `a?.b!`, the outermost chain is `a?.b` (`b` is the end of the chain starting at `a?.`)
1116+
* 3. For `(a?.b.c).d`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.` since parens end the chain)
1117+
* 4. For `a?.b.c?.d`, both `a?.b.c` and `a?.b.c?.d` are outermost (`c` is the end of the chain starting at `a?.`, and `d` is
11161118
* the end of the chain starting at `c?.`)
1117-
* 4. For `a?.(b?.c).d`, both `b?.c` and `a?.(b?.c)d` are outermost (`c` is the end of the chain starting at `b`, and `d` is
1119+
* 5. For `a?.(b?.c).d`, both `b?.c` and `a?.(b?.c)d` are outermost (`c` is the end of the chain starting at `b`, and `d` is
11181120
* the end of the chain starting at `a?.`)
11191121
*/
11201122
/* @internal */
11211123
export function isOutermostOptionalChain(node: OptionalChain) {
1122-
return !isOptionalChain(node.parent) // cases 1 and 2
1123-
|| isOptionalChainRoot(node.parent) // case 3
1124-
|| node !== node.parent.expression; // case 4
1124+
return !isOptionalChain(node.parent) // cases 1, 2, and 3
1125+
|| isOptionalChainRoot(node.parent) // case 4
1126+
|| node !== node.parent.expression; // case 5
11251127
}
11261128

11271129
export function isNullishCoalesce(node: Node) {
@@ -1152,11 +1154,7 @@ namespace ts {
11521154
export function skipPartiallyEmittedExpressions(node: Expression): Expression;
11531155
export function skipPartiallyEmittedExpressions(node: Node): Node;
11541156
export function skipPartiallyEmittedExpressions(node: Node) {
1155-
while (node.kind === SyntaxKind.PartiallyEmittedExpression) {
1156-
node = (<PartiallyEmittedExpression>node).expression;
1157-
}
1158-
1159-
return node;
1157+
return skipOuterExpressions(node, OuterExpressionKinds.PartiallyEmittedExpressions);
11601158
}
11611159

11621160
export function isFunctionExpression(node: Node): node is FunctionExpression {
@@ -1231,6 +1229,10 @@ namespace ts {
12311229
return node.kind === SyntaxKind.NonNullExpression;
12321230
}
12331231

1232+
export function isNonNullChain(node: Node): node is NonNullChain {
1233+
return isNonNullExpression(node) && !!(node.flags & NodeFlags.OptionalChain);
1234+
}
1235+
12341236
export function isMetaProperty(node: Node): node is MetaProperty {
12351237
return node.kind === SyntaxKind.MetaProperty;
12361238
}

Diff for: src/services/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1355,7 +1355,7 @@ namespace ts {
13551355
export function getPossibleGenericSignatures(called: Expression, typeArgumentCount: number, checker: TypeChecker): readonly Signature[] {
13561356
let type = checker.getTypeAtLocation(called);
13571357
if (isOptionalChain(called.parent)) {
1358-
type = removeOptionality(type, !!called.parent.questionDotToken, /*isOptionalChain*/ true);
1358+
type = removeOptionality(type, isOptionalChainRoot(called.parent), /*isOptionalChain*/ true);
13591359
}
13601360

13611361
const signatures = isNewExpression(called.parent) ? type.getConstructSignatures() : type.getCallSignatures();

0 commit comments

Comments
 (0)