Skip to content

Commit

Permalink
Treat '!' differently inside chain vs end of chain
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Feb 14, 2020
1 parent 0f8c1f7 commit 964daac
Show file tree
Hide file tree
Showing 25 changed files with 244 additions and 104 deletions.
24 changes: 20 additions & 4 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,9 @@ namespace ts {
case SyntaxKind.CallExpression:
bindCallExpressionFlow(<CallExpression>node);
break;
case SyntaxKind.NonNullExpression:
bindNonNullExpressionFlow(<NonNullExpression>node);
break;
case SyntaxKind.JSDocTypedefTag:
case SyntaxKind.JSDocCallbackTag:
case SyntaxKind.JSDocEnumTag:
Expand Down Expand Up @@ -1665,18 +1668,22 @@ namespace ts {
}

function bindOptionalChainRest(node: OptionalChain) {
bind(node.questionDotToken);
switch (node.kind) {
case SyntaxKind.PropertyAccessExpression:
bind(node.questionDotToken);
bind(node.name);
break;
case SyntaxKind.ElementAccessExpression:
bind(node.questionDotToken);
bind(node.argumentExpression);
break;
case SyntaxKind.CallExpression:
bind(node.questionDotToken);
bindEach(node.typeArguments);
bindEach(node.arguments);
break;
case SyntaxKind.NonNullExpression:
break;
}
}

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

function bindAccessExpressionFlow(node: AccessExpression) {
function bindNonNullExpressionFlow(node: NonNullExpression | NonNullChain) {
if (isOptionalChain(node)) {
bindOptionalChainFlow(node);
}
else {
bindEachChild(node);
}
}

function bindAccessExpressionFlow(node: AccessExpression | PropertyAccessChain | ElementAccessChain) {
if (isOptionalChain(node)) {
bindOptionalChainFlow(node);
}
Expand All @@ -1724,7 +1740,7 @@ namespace ts {
}
}

function bindCallExpressionFlow(node: CallExpression) {
function bindCallExpressionFlow(node: CallExpression | CallChain) {
if (isOptionalChain(node)) {
bindOptionalChainFlow(node);
}
Expand Down
9 changes: 8 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25900,8 +25900,15 @@ namespace ts {
return targetType;
}

function checkNonNullChain(node: NonNullChain) {
const leftType = checkExpression(node.expression);
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
return propagateOptionalTypeMarker(getNonNullableType(nonOptionalType), node, nonOptionalType !== leftType);
}

function checkNonNullAssertion(node: NonNullExpression) {
return getNonNullableType(checkExpression(node.expression));
return node.flags & NodeFlags.OptionalChain ? checkNonNullChain(node as NonNullChain) :
getNonNullableType(checkExpression(node.expression));
}

function checkMetaProperty(node: MetaProperty): Type {
Expand Down
18 changes: 11 additions & 7 deletions src/compiler/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,17 @@ namespace ts {
assertNode)
: noop;

export const assertNotNode = shouldAssert(AssertionLevel.Normal)
? (node: Node | undefined, test: ((node: Node | undefined) => boolean) | undefined, message?: string): void => assert(
test === undefined || !test(node),
message || "Unexpected node.",
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
assertNode)
: noop;
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>;
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string): void;
export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string): void {
if (shouldAssert(AssertionLevel.Normal)) {
assert(
test === undefined || node === undefined || !test(node),
message || "Unexpected node.",
() => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`,
assertNotNode);
}
}

export const assertOptionalNode = shouldAssert(AssertionLevel.Normal)
? (node: Node, test: (node: Node) => boolean, message?: string): void => assert(
Expand Down
33 changes: 9 additions & 24 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1335,9 +1335,11 @@ namespace ts {

export const enum OuterExpressionKinds {
Parentheses = 1 << 0,
Assertions = 1 << 1,
PartiallyEmittedExpressions = 1 << 2,
TypeAssertions = 1 << 1,
NonNullAssertions = 1 << 2,
PartiallyEmittedExpressions = 1 << 3,

Assertions = TypeAssertions | NonNullAssertions,
All = Parentheses | Assertions | PartiallyEmittedExpressions
}

Expand All @@ -1349,8 +1351,9 @@ namespace ts {
return (kinds & OuterExpressionKinds.Parentheses) !== 0;
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.AsExpression:
return (kinds & OuterExpressionKinds.TypeAssertions) !== 0;
case SyntaxKind.NonNullExpression:
return (kinds & OuterExpressionKinds.Assertions) !== 0;
return (kinds & OuterExpressionKinds.NonNullAssertions) !== 0;
case SyntaxKind.PartiallyEmittedExpression:
return (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) !== 0;
}
Expand All @@ -1360,34 +1363,16 @@ namespace ts {
export function skipOuterExpressions(node: Expression, kinds?: OuterExpressionKinds): Expression;
export function skipOuterExpressions(node: Node, kinds?: OuterExpressionKinds): Node;
export function skipOuterExpressions(node: Node, kinds = OuterExpressionKinds.All) {
let previousNode: Node;
do {
previousNode = node;
if (kinds & OuterExpressionKinds.Parentheses) {
node = skipParentheses(node);
}

if (kinds & OuterExpressionKinds.Assertions) {
node = skipAssertions(node);
}

if (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) {
node = skipPartiallyEmittedExpressions(node);
}
while (isOuterExpression(node, kinds)) {
node = node.expression;
}
while (previousNode !== node);

return node;
}

export function skipAssertions(node: Expression): Expression;
export function skipAssertions(node: Node): Node;
export function skipAssertions(node: Node): Node {
while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) {
node = (<AssertionExpression | NonNullExpression>node).expression;
}

return node;
return skipOuterExpressions(node, OuterExpressionKinds.Assertions);
}

function updateOuterExpression(outerExpression: OuterExpression, expression: Expression) {
Expand Down
23 changes: 19 additions & 4 deletions src/compiler/factoryPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1076,10 +1076,8 @@ namespace ts {
}

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

export function updateNonNullExpression(node: NonNullExpression, expression: Expression) {
if (isNonNullChain(node)) {
return updateNonNullChain(node, expression);
}
return node.expression !== expression
? updateNode(createNonNullExpression(expression), node)
: node;
}

export function createNonNullChain(expression: Expression) {
const node = <NonNullChain>createSynthesizedNode(SyntaxKind.NonNullExpression);
node.flags |= NodeFlags.OptionalChain;
node.expression = parenthesizeForAccess(expression);
return node;
}

export function updateNonNullChain(node: NonNullChain, expression: Expression) {
Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a NonNullExpression using updateNonNullChain. Use updateNonNullExpression instead.");
return node.expression !== expression
? updateNode(createNonNullChain(expression), node)
: node;
}

export function createMetaProperty(keywordToken: MetaProperty["keywordToken"], name: Identifier) {
const node = <MetaProperty>createSynthesizedNode(SyntaxKind.MetaProperty);
node.keywordToken = keywordToken;
Expand Down
50 changes: 38 additions & 12 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4685,20 +4685,12 @@ namespace ts {
&& lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate);
}

function hasOptionalChain(node: Node) {
while (true) {
if (node.flags & NodeFlags.OptionalChain) return true;
if (!isNonNullExpression(node)) return false;
node = node.expression;
}
}

function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) {
const propertyAccess = <PropertyAccessExpression>createNode(SyntaxKind.PropertyAccessExpression, expression.pos);
propertyAccess.expression = expression;
propertyAccess.questionDotToken = questionDotToken;
propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true);
if (questionDotToken || hasOptionalChain(expression)) {
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
propertyAccess.flags |= NodeFlags.OptionalChain;
if (isPrivateIdentifier(propertyAccess.name)) {
parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers);
Expand All @@ -4724,12 +4716,43 @@ namespace ts {
}

parseExpected(SyntaxKind.CloseBracketToken);
if (questionDotToken || hasOptionalChain(expression)) {
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
indexedAccess.flags |= NodeFlags.OptionalChain;
}
return finishNode(indexedAccess);
}

function nextTokenContinuesOptionalChainAfterExclamationToken() {
// consume a run of `!` tokens
while (token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) {
nextToken();
}
switch (token()) {
case SyntaxKind.DotToken:
case SyntaxKind.OpenBracketToken:
case SyntaxKind.OpenParenToken:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateHead:
case SyntaxKind.QuestionDotToken:
// a?.b!.c
// a?.b![c]
// a?.b!()
// a?.b!`` (illegal syntax in javascript but we must parse it)
// a?.b!`${c}` (illegal syntax in javascript but we must parse it)
// a?.b!?.c
// a?.b!?.[c]
// a?.b!?.()
return true;
case SyntaxKind.LessThanToken:
case SyntaxKind.LessThanLessThanToken:
// a?.b!<T>()
// a?.b!<<T>() (also handled in parseCallExpressionRest)
// look ahead to see if we are parsing a type argument list
return !!parseTypeArgumentsInExpression();
}
return false;
}

function parseMemberExpressionRest(expression: LeftHandSideExpression, allowOptionalChain: boolean): MemberExpression {
while (true) {
let questionDotToken: QuestionDotToken | undefined;
Expand All @@ -4751,6 +4774,9 @@ namespace ts {
nextToken();
const nonNullExpression = <NonNullExpression>createNode(SyntaxKind.NonNullExpression, expression.pos);
nonNullExpression.expression = expression;
if (expression.flags & NodeFlags.OptionalChain && lookAhead(nextTokenContinuesOptionalChainAfterExclamationToken)) {
nonNullExpression.flags |= NodeFlags.OptionalChain;
}
expression = finishNode(nonNullExpression);
continue;
}
Expand Down Expand Up @@ -4811,7 +4837,7 @@ namespace ts {
callExpr.questionDotToken = questionDotToken;
callExpr.typeArguments = typeArguments;
callExpr.arguments = parseArgumentList();
if (questionDotToken || hasOptionalChain(expression)) {
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
callExpr.flags |= NodeFlags.OptionalChain;
}
expression = finishNode(callExpr);
Expand All @@ -4823,7 +4849,7 @@ namespace ts {
callExpr.expression = expression;
callExpr.questionDotToken = questionDotToken;
callExpr.arguments = parseArgumentList();
if (questionDotToken || hasOptionalChain(expression)) {
if (questionDotToken || expression.flags & NodeFlags.OptionalChain) {
callExpr.flags |= NodeFlags.OptionalChain;
}
expression = finishNode(callExpr);
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/transformers/es2020.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ namespace ts {
}

function flattenChain(chain: OptionalChain) {
Debug.assertNotNode(chain, isNonNullChain);
const links: OptionalChain[] = [chain];
while (!chain.questionDotToken && !isTaggedTemplateExpression(chain)) {
chain = cast(skipPartiallyEmittedExpressions(chain.expression), isOptionalChain);
Debug.assertNotNode(chain, isNonNullChain);
links.unshift(chain);
}
return { expression: chain.expression, chain: links };
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,7 @@ namespace ts {
| PropertyAccessChain
| ElementAccessChain
| CallChain
| NonNullChain
;

/* @internal */
Expand Down Expand Up @@ -2016,6 +2017,10 @@ namespace ts {
expression: Expression;
}

export interface NonNullChain extends NonNullExpression {
_optionalChainBrand: any;
}

// NOTE: MetaProperty is really a MemberExpression, but we consider it a PrimaryExpression
// for the same reasons we treat NewExpression as a PrimaryExpression.
export interface MetaProperty extends PrimaryExpression {
Expand Down
6 changes: 1 addition & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2614,11 +2614,7 @@ namespace ts {
export function skipParentheses(node: Expression): Expression;
export function skipParentheses(node: Node): Node;
export function skipParentheses(node: Node): Node {
while (node.kind === SyntaxKind.ParenthesizedExpression) {
node = (node as ParenthesizedExpression).expression;
}

return node;
return skipOuterExpressions(node, OuterExpressionKinds.Parentheses);
}

function skipParenthesesUp(node: Node): Node {
Expand Down
Loading

0 comments on commit 964daac

Please sign in to comment.