Skip to content

Commit 964daac

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

25 files changed

+244
-104
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

+38-12
Original file line numberDiff line numberDiff line change
@@ -4685,20 +4685,12 @@ 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;
4693-
}
4694-
}
4695-
46964688
function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) {
46974689
const propertyAccess = <PropertyAccessExpression>createNode(SyntaxKind.PropertyAccessExpression, expression.pos);
46984690
propertyAccess.expression = expression;
46994691
propertyAccess.questionDotToken = questionDotToken;
47004692
propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true);
4701-
if (questionDotToken || hasOptionalChain(expression)) {
4693+
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
47024694
propertyAccess.flags |= NodeFlags.OptionalChain;
47034695
if (isPrivateIdentifier(propertyAccess.name)) {
47044696
parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers);
@@ -4724,12 +4716,43 @@ namespace ts {
47244716
}
47254717

47264718
parseExpected(SyntaxKind.CloseBracketToken);
4727-
if (questionDotToken || hasOptionalChain(expression)) {
4719+
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
47284720
indexedAccess.flags |= NodeFlags.OptionalChain;
47294721
}
47304722
return finishNode(indexedAccess);
47314723
}
47324724

4725+
function nextTokenContinuesOptionalChainAfterExclamationToken() {
4726+
// consume a run of `!` tokens
4727+
while (token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) {
4728+
nextToken();
4729+
}
4730+
switch (token()) {
4731+
case SyntaxKind.DotToken:
4732+
case SyntaxKind.OpenBracketToken:
4733+
case SyntaxKind.OpenParenToken:
4734+
case SyntaxKind.NoSubstitutionTemplateLiteral:
4735+
case SyntaxKind.TemplateHead:
4736+
case SyntaxKind.QuestionDotToken:
4737+
// a?.b!.c
4738+
// a?.b![c]
4739+
// a?.b!()
4740+
// a?.b!`` (illegal syntax in javascript but we must parse it)
4741+
// a?.b!`${c}` (illegal syntax in javascript but we must parse it)
4742+
// a?.b!?.c
4743+
// a?.b!?.[c]
4744+
// a?.b!?.()
4745+
return true;
4746+
case SyntaxKind.LessThanToken:
4747+
case SyntaxKind.LessThanLessThanToken:
4748+
// a?.b!<T>()
4749+
// a?.b!<<T>() (also handled in parseCallExpressionRest)
4750+
// look ahead to see if we are parsing a type argument list
4751+
return !!parseTypeArgumentsInExpression();
4752+
}
4753+
return false;
4754+
}
4755+
47334756
function parseMemberExpressionRest(expression: LeftHandSideExpression, allowOptionalChain: boolean): MemberExpression {
47344757
while (true) {
47354758
let questionDotToken: QuestionDotToken | undefined;
@@ -4751,6 +4774,9 @@ namespace ts {
47514774
nextToken();
47524775
const nonNullExpression = <NonNullExpression>createNode(SyntaxKind.NonNullExpression, expression.pos);
47534776
nonNullExpression.expression = expression;
4777+
if (expression.flags & NodeFlags.OptionalChain && lookAhead(nextTokenContinuesOptionalChainAfterExclamationToken)) {
4778+
nonNullExpression.flags |= NodeFlags.OptionalChain;
4779+
}
47544780
expression = finishNode(nonNullExpression);
47554781
continue;
47564782
}
@@ -4811,7 +4837,7 @@ namespace ts {
48114837
callExpr.questionDotToken = questionDotToken;
48124838
callExpr.typeArguments = typeArguments;
48134839
callExpr.arguments = parseArgumentList();
4814-
if (questionDotToken || hasOptionalChain(expression)) {
4840+
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
48154841
callExpr.flags |= NodeFlags.OptionalChain;
48164842
}
48174843
expression = finishNode(callExpr);
@@ -4823,7 +4849,7 @@ namespace ts {
48234849
callExpr.expression = expression;
48244850
callExpr.questionDotToken = questionDotToken;
48254851
callExpr.arguments = parseArgumentList();
4826-
if (questionDotToken || hasOptionalChain(expression)) {
4852+
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
48274853
callExpr.flags |= NodeFlags.OptionalChain;
48284854
}
48294855
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 {

0 commit comments

Comments
 (0)