Skip to content

Commit fcd9334

Browse files
authored
Add support for Optional Chaining (#33294)
* Add support for Optional Chaining * Add grammar error for invalid tagged template, more tests * Prototype * PR feedback * Add errors for invalid assignments and a trailing '?.' * Add additional signature help test, fix lint warnings * Fix to insert text for completions * Add initial control-flow analysis for optional chains * PR Feedback and more tests * Update to control flow * Remove mangled smart quotes in comments * Fix lint, PR feedback * Updates to control flow * Switch to FlowCondition for CFA of optional chains * Fix ?. insertion for completions on type variables * Accept API baseline change * Clean up types * improve control-flow debug output * Revert Debug.formatControlFlowGraph helper
1 parent 7ce793c commit fcd9334

File tree

76 files changed

+6422
-885
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+6422
-885
lines changed

Diff for: src/compiler/binder.ts

+121-24
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,10 @@ namespace ts {
797797
case SyntaxKind.VariableDeclaration:
798798
bindVariableDeclarationFlow(<VariableDeclaration>node);
799799
break;
800+
case SyntaxKind.PropertyAccessExpression:
801+
case SyntaxKind.ElementAccessExpression:
802+
bindAccessExpressionFlow(<AccessExpression>node);
803+
break;
800804
case SyntaxKind.CallExpression:
801805
bindCallExpressionFlow(<CallExpression>node);
802806
break;
@@ -941,7 +945,9 @@ namespace ts {
941945
}
942946
if (expression.kind === SyntaxKind.TrueKeyword && flags & FlowFlags.FalseCondition ||
943947
expression.kind === SyntaxKind.FalseKeyword && flags & FlowFlags.TrueCondition) {
944-
return unreachableFlow;
948+
if (!isOptionalChainRoot(expression.parent)) {
949+
return unreachableFlow;
950+
}
945951
}
946952
if (!isNarrowingExpression(expression)) {
947953
return antecedent;
@@ -1015,23 +1021,28 @@ namespace ts {
10151021
}
10161022

10171023
function isTopLevelLogicalExpression(node: Node): boolean {
1018-
while (node.parent.kind === SyntaxKind.ParenthesizedExpression ||
1019-
node.parent.kind === SyntaxKind.PrefixUnaryExpression &&
1020-
(<PrefixUnaryExpression>node.parent).operator === SyntaxKind.ExclamationToken) {
1024+
while (isParenthesizedExpression(node.parent) ||
1025+
isPrefixUnaryExpression(node.parent) && node.parent.operator === SyntaxKind.ExclamationToken) {
10211026
node = node.parent;
10221027
}
1023-
return !isStatementCondition(node) && !isLogicalExpression(node.parent);
1028+
return !isStatementCondition(node) &&
1029+
!isLogicalExpression(node.parent) &&
1030+
!(isOptionalChain(node.parent) && node.parent.expression === node);
10241031
}
10251032

1026-
function bindCondition(node: Expression | undefined, trueTarget: FlowLabel, falseTarget: FlowLabel) {
1027-
const saveTrueTarget = currentTrueTarget;
1028-
const saveFalseTarget = currentFalseTarget;
1033+
function doWithConditionalBranches<T>(action: (value: T) => void, value: T, trueTarget: FlowLabel, falseTarget: FlowLabel) {
1034+
const savedTrueTarget = currentTrueTarget;
1035+
const savedFalseTarget = currentFalseTarget;
10291036
currentTrueTarget = trueTarget;
10301037
currentFalseTarget = falseTarget;
1031-
bind(node);
1032-
currentTrueTarget = saveTrueTarget;
1033-
currentFalseTarget = saveFalseTarget;
1034-
if (!node || !isLogicalExpression(node)) {
1038+
action(value);
1039+
currentTrueTarget = savedTrueTarget;
1040+
currentFalseTarget = savedFalseTarget;
1041+
}
1042+
1043+
function bindCondition(node: Expression | undefined, trueTarget: FlowLabel, falseTarget: FlowLabel) {
1044+
doWithConditionalBranches(bind, node, trueTarget, falseTarget);
1045+
if (!node || !isLogicalExpression(node) && !(isOptionalChain(node) && isOutermostOptionalChain(node))) {
10351046
addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node));
10361047
addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node));
10371048
}
@@ -1536,22 +1547,96 @@ namespace ts {
15361547
}
15371548
}
15381549

1539-
function bindCallExpressionFlow(node: CallExpression) {
1540-
// If the target of the call expression is a function expression or arrow function we have
1541-
// an immediately invoked function expression (IIFE). Initialize the flowNode property to
1542-
// the current control flow (which includes evaluation of the IIFE arguments).
1543-
let expr: Expression = node.expression;
1544-
while (expr.kind === SyntaxKind.ParenthesizedExpression) {
1545-
expr = (<ParenthesizedExpression>expr).expression;
1546-
}
1547-
if (expr.kind === SyntaxKind.FunctionExpression || expr.kind === SyntaxKind.ArrowFunction) {
1548-
bindEach(node.typeArguments);
1549-
bindEach(node.arguments);
1550-
bind(node.expression);
1550+
function isOutermostOptionalChain(node: OptionalChain) {
1551+
return !isOptionalChain(node.parent) || isOptionalChainRoot(node.parent) || node !== node.parent.expression;
1552+
}
1553+
1554+
function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) {
1555+
doWithConditionalBranches(bind, node, trueTarget, falseTarget);
1556+
if (!isOptionalChain(node) || isOutermostOptionalChain(node)) {
1557+
addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node));
1558+
addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node));
1559+
}
1560+
}
1561+
1562+
function bindOptionalChainRest(node: OptionalChain) {
1563+
bind(node.questionDotToken);
1564+
switch (node.kind) {
1565+
case SyntaxKind.PropertyAccessExpression:
1566+
bind(node.name);
1567+
break;
1568+
case SyntaxKind.ElementAccessExpression:
1569+
bind(node.argumentExpression);
1570+
break;
1571+
case SyntaxKind.CallExpression:
1572+
bindEach(node.typeArguments);
1573+
bindEach(node.arguments);
1574+
break;
1575+
}
1576+
}
1577+
1578+
function bindOptionalChain(node: OptionalChain, trueTarget: FlowLabel, falseTarget: FlowLabel) {
1579+
// For an optional chain, we emulate the behavior of a logical expression:
1580+
//
1581+
// a?.b -> a && a.b
1582+
// a?.b.c -> a && a.b.c
1583+
// a?.b?.c -> a && a.b && a.b.c
1584+
// a?.[x = 1] -> a && a[x = 1]
1585+
//
1586+
// To do this we descend through the chain until we reach the root of a chain (the expression with a `?.`)
1587+
// and build it's CFA graph as if it were the first condition (`a && ...`). Then we bind the rest
1588+
// of the node as part of the "true" branch, and continue to do so as we ascend back up to the outermost
1589+
// chain node. We then treat the entire node as the right side of the expression.
1590+
const preChainLabel = node.questionDotToken ? createBranchLabel() : undefined;
1591+
bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget);
1592+
if (preChainLabel) {
1593+
currentFlow = finishFlowLabel(preChainLabel);
1594+
}
1595+
doWithConditionalBranches(bindOptionalChainRest, node, trueTarget, falseTarget);
1596+
if (isOutermostOptionalChain(node)) {
1597+
addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node));
1598+
addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node));
1599+
}
1600+
}
1601+
1602+
function bindOptionalChainFlow(node: OptionalChain) {
1603+
if (isTopLevelLogicalExpression(node)) {
1604+
const postExpressionLabel = createBranchLabel();
1605+
bindOptionalChain(node, postExpressionLabel, postExpressionLabel);
1606+
currentFlow = finishFlowLabel(postExpressionLabel);
1607+
}
1608+
else {
1609+
bindOptionalChain(node, currentTrueTarget!, currentFalseTarget!);
1610+
}
1611+
}
1612+
1613+
function bindAccessExpressionFlow(node: AccessExpression) {
1614+
if (isOptionalChain(node)) {
1615+
bindOptionalChainFlow(node);
15511616
}
15521617
else {
15531618
bindEachChild(node);
15541619
}
1620+
}
1621+
1622+
function bindCallExpressionFlow(node: CallExpression) {
1623+
if (isOptionalChain(node)) {
1624+
bindOptionalChainFlow(node);
1625+
}
1626+
else {
1627+
// If the target of the call expression is a function expression or arrow function we have
1628+
// an immediately invoked function expression (IIFE). Initialize the flowNode property to
1629+
// the current control flow (which includes evaluation of the IIFE arguments).
1630+
const expr = skipParentheses(node.expression);
1631+
if (expr.kind === SyntaxKind.FunctionExpression || expr.kind === SyntaxKind.ArrowFunction) {
1632+
bindEach(node.typeArguments);
1633+
bindEach(node.arguments);
1634+
bind(node.expression);
1635+
}
1636+
else {
1637+
bindEachChild(node);
1638+
}
1639+
}
15551640
if (node.expression.kind === SyntaxKind.PropertyAccessExpression) {
15561641
const propertyAccess = <PropertyAccessExpression>node.expression;
15571642
if (isNarrowableOperand(propertyAccess.expression) && isPushOrUnshiftIdentifier(propertyAccess.name)) {
@@ -3297,6 +3382,10 @@ namespace ts {
32973382
const callee = skipOuterExpressions(node.expression);
32983383
const expression = node.expression;
32993384

3385+
if (node.flags & NodeFlags.OptionalChain) {
3386+
transformFlags |= TransformFlags.ContainsESNext;
3387+
}
3388+
33003389
if (node.typeArguments) {
33013390
transformFlags |= TransformFlags.AssertTypeScript;
33023391
}
@@ -3692,6 +3781,10 @@ namespace ts {
36923781
function computePropertyAccess(node: PropertyAccessExpression, subtreeFlags: TransformFlags) {
36933782
let transformFlags = subtreeFlags;
36943783

3784+
if (node.flags & NodeFlags.OptionalChain) {
3785+
transformFlags |= TransformFlags.ContainsESNext;
3786+
}
3787+
36953788
// If a PropertyAccessExpression starts with a super keyword, then it is
36963789
// ES6 syntax, and requires a lexical `this` binding.
36973790
if (node.expression.kind === SyntaxKind.SuperKeyword) {
@@ -3707,6 +3800,10 @@ namespace ts {
37073800
function computeElementAccess(node: ElementAccessExpression, subtreeFlags: TransformFlags) {
37083801
let transformFlags = subtreeFlags;
37093802

3803+
if (node.flags & NodeFlags.OptionalChain) {
3804+
transformFlags |= TransformFlags.ContainsESNext;
3805+
}
3806+
37103807
// If an ElementAccessExpression starts with a super keyword, then it is
37113808
// ES6 syntax, and requires a lexical `this` binding.
37123809
if (node.expression.kind === SyntaxKind.SuperKeyword) {

0 commit comments

Comments
 (0)