Skip to content

Commit d187ca7

Browse files
committed
Treat '!' differently inside chain vs end of chain
1 parent 0f8c1f7 commit d187ca7

25 files changed

+229
-101
lines changed

Diff for: src/compiler/binder.ts

+20-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:
@@ -1665,18 +1668,22 @@ namespace ts {
16651668
}
16661669

16671670
function bindOptionalChainRest(node: OptionalChain) {
1668-
bind(node.questionDotToken);
16691671
switch (node.kind) {
16701672
case SyntaxKind.PropertyAccessExpression:
1673+
bind(node.questionDotToken);
16711674
bind(node.name);
16721675
break;
16731676
case SyntaxKind.ElementAccessExpression:
1677+
bind(node.questionDotToken);
16741678
bind(node.argumentExpression);
16751679
break;
16761680
case SyntaxKind.CallExpression:
1681+
bind(node.questionDotToken);
16771682
bindEach(node.typeArguments);
16781683
bindEach(node.arguments);
16791684
break;
1685+
case SyntaxKind.NonNullExpression:
1686+
break;
16801687
}
16811688
}
16821689

@@ -1692,7 +1699,7 @@ namespace ts {
16921699
// and build it's CFA graph as if it were the first condition (`a && ...`). Then we bind the rest
16931700
// of the node as part of the "true" branch, and continue to do so as we ascend back up to the outermost
16941701
// chain node. We then treat the entire node as the right side of the expression.
1695-
const preChainLabel = node.questionDotToken ? createBranchLabel() : undefined;
1702+
const preChainLabel = isOptionalChainRoot(node) ? createBranchLabel() : undefined;
16961703
bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget);
16971704
if (preChainLabel) {
16981705
currentFlow = finishFlowLabel(preChainLabel);
@@ -1715,7 +1722,16 @@ namespace ts {
17151722
}
17161723
}
17171724

1718-
function bindAccessExpressionFlow(node: AccessExpression) {
1725+
function bindNonNullExpressionFlow(node: NonNullExpression | NonNullChain) {
1726+
if (isOptionalChain(node)) {
1727+
bindOptionalChainFlow(node);
1728+
}
1729+
else {
1730+
bindEachChild(node);
1731+
}
1732+
}
1733+
1734+
function bindAccessExpressionFlow(node: AccessExpression | PropertyAccessChain | ElementAccessChain) {
17191735
if (isOptionalChain(node)) {
17201736
bindOptionalChainFlow(node);
17211737
}
@@ -1724,7 +1740,7 @@ namespace ts {
17241740
}
17251741
}
17261742

1727-
function bindCallExpressionFlow(node: CallExpression) {
1743+
function bindCallExpressionFlow(node: CallExpression | CallChain) {
17281744
if (isOptionalChain(node)) {
17291745
bindOptionalChainFlow(node);
17301746
}

Diff for: src/compiler/checker.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -25900,8 +25900,15 @@ namespace ts {
2590025900
return targetType;
2590125901
}
2590225902

25903+
function checkNonNullChain(node: NonNullChain) {
25904+
const leftType = checkExpression(node.expression);
25905+
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
25906+
return propagateOptionalTypeMarker(getNonNullableType(nonOptionalType), node, nonOptionalType !== leftType);
25907+
}
25908+
2590325909
function checkNonNullAssertion(node: NonNullExpression) {
25904-
return getNonNullableType(checkExpression(node.expression));
25910+
return node.flags & NodeFlags.OptionalChain ? checkNonNullChain(node as NonNullChain) :
25911+
getNonNullableType(checkExpression(node.expression));
2590525912
}
2590625913

2590725914
function checkMetaProperty(node: MetaProperty): Type {

Diff for: src/compiler/debug.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,17 @@ namespace ts {
189189
assertNode)
190190
: noop;
191191

192-
export const assertNotNode = shouldAssert(AssertionLevel.Normal)
193-
? (node: Node | undefined, test: ((node: Node | undefined) => boolean) | undefined, message?: string): void => assert(
194-
test === undefined || !test(node),
195-
message || "Unexpected node.",
196-
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
197-
assertNode)
198-
: noop;
192+
export function assertNotNode<T extends Node, U extends T>(node: T | undefined, test: (node: Node) => node is U, message?: string): asserts node is Exclude<T, U>;
193+
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string): void;
194+
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string): void {
195+
if (shouldAssert(AssertionLevel.Normal)) {
196+
assert(
197+
test === undefined || node === undefined || !test(node),
198+
message || "Unexpected node.",
199+
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
200+
assertNotNode);
201+
}
202+
}
199203

200204
export const assertOptionalNode = shouldAssert(AssertionLevel.Normal)
201205
? (node: Node, test: (node: Node) => boolean, message?: string): void => assert(

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

+23-9
Original file line numberDiff line numberDiff line change
@@ -4685,20 +4685,34 @@ namespace ts {
46854685
&& lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate);
46864686
}
46874687

4688-
function hasOptionalChain(node: Node) {
4689-
while (true) {
4690-
if (node.flags & NodeFlags.OptionalChain) return true;
4691-
if (!isNonNullExpression(node)) return false;
4692-
node = node.expression;
4688+
function tryReparseOptionalChain(node: Expression) {
4689+
if (node.flags & NodeFlags.OptionalChain) {
4690+
return true;
46934691
}
4692+
// check for an optional chain in a non-null expression
4693+
if (isNonNullExpression(node)) {
4694+
let expr = node.expression;
4695+
while (isNonNullExpression(expr) && !(expr.flags & NodeFlags.OptionalChain)) {
4696+
expr = expr.expression;
4697+
}
4698+
if (expr.flags & NodeFlags.OptionalChain) {
4699+
// this is part of an optional chain. Walk down from `node` to `expression` and set the flag.
4700+
while (isNonNullExpression(node)) {
4701+
node.flags |= NodeFlags.OptionalChain;
4702+
node = node.expression;
4703+
}
4704+
return true;
4705+
}
4706+
}
4707+
return false;
46944708
}
46954709

46964710
function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) {
46974711
const propertyAccess = <PropertyAccessExpression>createNode(SyntaxKind.PropertyAccessExpression, expression.pos);
46984712
propertyAccess.expression = expression;
46994713
propertyAccess.questionDotToken = questionDotToken;
47004714
propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true);
4701-
if (questionDotToken || hasOptionalChain(expression)) {
4715+
if (questionDotToken || tryReparseOptionalChain(expression)) {
47024716
propertyAccess.flags |= NodeFlags.OptionalChain;
47034717
if (isPrivateIdentifier(propertyAccess.name)) {
47044718
parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers);
@@ -4724,7 +4738,7 @@ namespace ts {
47244738
}
47254739

47264740
parseExpected(SyntaxKind.CloseBracketToken);
4727-
if (questionDotToken || hasOptionalChain(expression)) {
4741+
if (questionDotToken || tryReparseOptionalChain(expression)) {
47284742
indexedAccess.flags |= NodeFlags.OptionalChain;
47294743
}
47304744
return finishNode(indexedAccess);
@@ -4811,7 +4825,7 @@ namespace ts {
48114825
callExpr.questionDotToken = questionDotToken;
48124826
callExpr.typeArguments = typeArguments;
48134827
callExpr.arguments = parseArgumentList();
4814-
if (questionDotToken || hasOptionalChain(expression)) {
4828+
if (questionDotToken || tryReparseOptionalChain(expression)) {
48154829
callExpr.flags |= NodeFlags.OptionalChain;
48164830
}
48174831
expression = finishNode(callExpr);
@@ -4823,7 +4837,7 @@ namespace ts {
48234837
callExpr.expression = expression;
48244838
callExpr.questionDotToken = questionDotToken;
48254839
callExpr.arguments = parseArgumentList();
4826-
if (questionDotToken || hasOptionalChain(expression)) {
4840+
if (questionDotToken || tryReparseOptionalChain(expression)) {
48274841
callExpr.flags |= NodeFlags.OptionalChain;
48284842
}
48294843
expression = finishNode(callExpr);

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

+2
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)) {
4748
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
@@ -1922,6 +1922,7 @@ namespace ts {
19221922
| PropertyAccessChain
19231923
| ElementAccessChain
19241924
| CallChain
1925+
| NonNullChain
19251926
;
19261927

19271928
/* @internal */
@@ -2016,6 +2017,10 @@ namespace ts {
20162017
expression: Expression;
20172018
}
20182019

2020+
export interface NonNullChain extends NonNullExpression {
2021+
_optionalChainBrand: any;
2022+
}
2023+
20192024
// NOTE: MetaProperty is really a MemberExpression, but we consider it a PrimaryExpression
20202025
// for the same reasons we treat NewExpression as a PrimaryExpression.
20212026
export interface MetaProperty extends PrimaryExpression {

Diff for: src/compiler/utilities.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -2614,11 +2614,7 @@ namespace ts {
26142614
export function skipParentheses(node: Expression): Expression;
26152615
export function skipParentheses(node: Node): Node;
26162616
export function skipParentheses(node: Node): Node {
2617-
while (node.kind === SyntaxKind.ParenthesizedExpression) {
2618-
node = (node as ParenthesizedExpression).expression;
2619-
}
2620-
2621-
return node;
2617+
return skipOuterExpressions(node, OuterExpressionKinds.Parentheses);
26222618
}
26232619

26242620
function skipParenthesesUp(node: Node): Node {

Diff for: src/compiler/utilitiesPublic.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -1076,17 +1076,18 @@ namespace ts {
10761076
return isCallExpression(node) && !!(node.flags & NodeFlags.OptionalChain);
10771077
}
10781078

1079-
export function isOptionalChain(node: Node): node is PropertyAccessChain | ElementAccessChain | CallChain {
1079+
export function isOptionalChain(node: Node): node is PropertyAccessChain | ElementAccessChain | CallChain | NonNullChain {
10801080
const kind = node.kind;
10811081
return !!(node.flags & NodeFlags.OptionalChain) &&
10821082
(kind === SyntaxKind.PropertyAccessExpression
10831083
|| kind === SyntaxKind.ElementAccessExpression
1084-
|| kind === SyntaxKind.CallExpression);
1084+
|| kind === SyntaxKind.CallExpression
1085+
|| kind === SyntaxKind.NonNullExpression);
10851086
}
10861087

10871088
/* @internal */
10881089
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
1089-
return isOptionalChain(node) && !!node.questionDotToken;
1090+
return isOptionalChain(node) && !isNonNullExpression(node) && !!node.questionDotToken;
10901091
}
10911092

10921093
/**
@@ -1101,17 +1102,18 @@ namespace ts {
11011102
* Determines whether a node is the outermost `OptionalChain` in an ECMAScript `OptionalExpression`:
11021103
*
11031104
* 1. For `a?.b.c`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.`)
1104-
* 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)
1105-
* 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
1105+
* 2. For `a?.b!`, the outermost chain is `a?.b!` (`c` is the end of the chain starting at `a?.`)
1106+
* 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)
1107+
* 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
11061108
* the end of the chain starting at `c?.`)
1107-
* 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
1109+
* 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
11081110
* the end of the chain starting at `a?.`)
11091111
*/
11101112
/* @internal */
11111113
export function isOutermostOptionalChain(node: OptionalChain) {
1112-
return !isOptionalChain(node.parent) // cases 1 and 2
1113-
|| isOptionalChainRoot(node.parent) // case 3
1114-
|| node !== node.parent.expression; // case 4
1114+
return !isOptionalChain(node.parent) // cases 1, 2, and 3
1115+
|| isOptionalChainRoot(node.parent) // case 4
1116+
|| node !== node.parent.expression; // case 5
11151117
}
11161118

11171119
export function isNullishCoalesce(node: Node) {
@@ -1142,11 +1144,7 @@ namespace ts {
11421144
export function skipPartiallyEmittedExpressions(node: Expression): Expression;
11431145
export function skipPartiallyEmittedExpressions(node: Node): Node;
11441146
export function skipPartiallyEmittedExpressions(node: Node) {
1145-
while (node.kind === SyntaxKind.PartiallyEmittedExpression) {
1146-
node = (<PartiallyEmittedExpression>node).expression;
1147-
}
1148-
1149-
return node;
1147+
return skipOuterExpressions(node, OuterExpressionKinds.PartiallyEmittedExpressions);
11501148
}
11511149

11521150
export function isFunctionExpression(node: Node): node is FunctionExpression {
@@ -1221,6 +1219,10 @@ namespace ts {
12211219
return node.kind === SyntaxKind.NonNullExpression;
12221220
}
12231221

1222+
export function isNonNullChain(node: Node): node is NonNullChain {
1223+
return isNonNullExpression(node) && !!(node.flags & NodeFlags.OptionalChain);
1224+
}
1225+
12241226
export function isMetaProperty(node: Node): node is MetaProperty {
12251227
return node.kind === SyntaxKind.MetaProperty;
12261228
}

0 commit comments

Comments
 (0)