Skip to content

Commit 7c50bcc

Browse files
KingwlRyanCavanaugh
authored andcommitted
nullish coalescing commit (#32883)
* migrate nullish coalescing commit * add more test case * add branch type check test * add more tests * fix nullish precedence * update public api * add rescan question question token to fix regression * update public api baseline * Added tests that emit for nullish coalescing operator conforming with grammar restrictions when assertions are used. * Fixed emit to hoist temporary variables (they previously went undeclared). Added tests to ensure calls and property accesses are only called once. * use not equal to null * rename factory * add grammar check * fix more cases * Fix handling of nullish coalescing oprator in expando objects. * Fixed classifier to support ?? operator. * update baseline * accept baseline * fix review * update emitter and more testcase * update control flow * make linter happy * update libs * avoid unnecessary assert * fix typooo * Fixes for control-flow analysis
1 parent 6bd3b21 commit 7c50bcc

File tree

85 files changed

+4043
-604
lines changed

Some content is hidden

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

85 files changed

+4043
-604
lines changed

src/compiler/binder.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,8 @@ namespace ts {
10151015
else {
10161016
return node.kind === SyntaxKind.BinaryExpression && (
10171017
(<BinaryExpression>node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken ||
1018-
(<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken);
1018+
(<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken ||
1019+
(<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken);
10191020
}
10201021
}
10211022
}
@@ -1466,7 +1467,7 @@ namespace ts {
14661467

14671468
function bindBinaryExpressionFlow(node: BinaryExpression) {
14681469
const operator = node.operatorToken.kind;
1469-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken) {
1470+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
14701471
if (isTopLevelLogicalExpression(node)) {
14711472
const postExpressionLabel = createBranchLabel();
14721473
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
@@ -2948,7 +2949,7 @@ namespace ts {
29482949
init = init && getRightMostAssignedExpression(init);
29492950
if (init) {
29502951
const isPrototypeAssignment = isPrototypeAccess(isVariableDeclaration(node) ? node.name : isBinaryExpression(node) ? node.left : node);
2951-
return !!getExpandoInitializer(isBinaryExpression(init) && init.operatorToken.kind === SyntaxKind.BarBarToken ? init.right : init, isPrototypeAssignment);
2952+
return !!getExpandoInitializer(isBinaryExpression(init) && (init.operatorToken.kind === SyntaxKind.BarBarToken || init.operatorToken.kind === SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment);
29522953
}
29532954
return false;
29542955
}
@@ -3424,7 +3425,10 @@ namespace ts {
34243425
const operatorTokenKind = node.operatorToken.kind;
34253426
const leftKind = node.left.kind;
34263427

3427-
if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) {
3428+
if (operatorTokenKind === SyntaxKind.QuestionQuestionToken) {
3429+
transformFlags |= TransformFlags.AssertESNext;
3430+
}
3431+
else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) {
34283432
// Destructuring object assignments with are ES2015 syntax
34293433
// and possibly ES2018 if they contain rest
34303434
transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment;

src/compiler/checker.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -13254,7 +13254,7 @@ namespace ts {
1325413254
return isContextSensitive((<ConditionalExpression>node).whenTrue) ||
1325513255
isContextSensitive((<ConditionalExpression>node).whenFalse);
1325613256
case SyntaxKind.BinaryExpression:
13257-
return (<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken &&
13257+
return ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken || (<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) &&
1325813258
(isContextSensitive((<BinaryExpression>node).left) || isContextSensitive((<BinaryExpression>node).right));
1325913259
case SyntaxKind.PropertyAssignment:
1326013260
return isContextSensitive((<PropertyAssignment>node).initializer);
@@ -19697,7 +19697,8 @@ namespace ts {
1969719697
// will be a subtype or the same type as the argument.
1969819698
function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type {
1969919699
// for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a`
19700-
if (isOptionalChainRoot(expr.parent)) {
19700+
if (isOptionalChainRoot(expr.parent) ||
19701+
isBinaryExpression(expr.parent) && expr.parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken && expr.parent.left === expr) {
1970119702
return narrowTypeByOptionality(type, expr, assumeTrue);
1970219703
}
1970319704
switch (expr.kind) {
@@ -20903,6 +20904,7 @@ namespace ts {
2090320904
}
2090420905
return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive;
2090520906
case SyntaxKind.BarBarToken:
20907+
case SyntaxKind.QuestionQuestionToken:
2090620908
// When an || expression has a contextual type, the operands are contextually typed by that type, except
2090720909
// when that type originates in a binding pattern, the right operand is contextually typed by the type of
2090820910
// the left operand. When an || expression has no contextual type, the right operand is contextually typed
@@ -26471,16 +26473,29 @@ namespace ts {
2647126473
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
2647226474
return checkExpression(node.right, checkMode);
2647326475
}
26476+
checkGrammarNullishCoalesceWithLogicalExpression(node);
2647426477
return checkBinaryLikeExpression(node.left, node.operatorToken, node.right, checkMode, node);
2647526478
}
2647626479

26480+
function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) {
26481+
const { left, operatorToken, right } = node;
26482+
if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) {
26483+
if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
26484+
grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind));
26485+
}
26486+
if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
26487+
grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind));
26488+
}
26489+
}
26490+
}
26491+
2647726492
function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): Type {
2647826493
const operator = operatorToken.kind;
2647926494
if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) {
2648026495
return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === SyntaxKind.ThisKeyword);
2648126496
}
2648226497
let leftType: Type;
26483-
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken) {
26498+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
2648426499
leftType = checkTruthinessExpression(left, checkMode);
2648526500
}
2648626501
else {
@@ -26641,6 +26656,10 @@ namespace ts {
2664126656
return getTypeFacts(leftType) & TypeFacts.Falsy ?
2664226657
getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], UnionReduction.Subtype) :
2664326658
leftType;
26659+
case SyntaxKind.QuestionQuestionToken:
26660+
return getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ?
26661+
getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) :
26662+
leftType;
2664426663
case SyntaxKind.EqualsToken:
2664526664
const declKind = isBinaryExpression(left.parent) ? getAssignmentDeclarationKind(left.parent) : AssignmentDeclarationKind.None;
2664626665
checkAssignmentDeclaration(declKind, rightType);

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -3273,6 +3273,10 @@
32733273
"category": "Error",
32743274
"code": 5075
32753275
},
3276+
"'{0}' and '{1}' operations cannot be mixed without parentheses.": {
3277+
"category": "Error",
3278+
"code": 5076
3279+
},
32763280

32773281
"Generates a sourcemap for each corresponding '.d.ts' file.": {
32783282
"category": "Message",

src/compiler/factory.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3248,6 +3248,10 @@ namespace ts {
32483248
return createBinary(left, SyntaxKind.BarBarToken, right);
32493249
}
32503250

3251+
export function createNullishCoalesce(left: Expression, right: Expression) {
3252+
return createBinary(left, SyntaxKind.QuestionQuestionToken, right);
3253+
}
3254+
32513255
export function createLogicalNot(operand: Expression) {
32523256
return createPrefix(SyntaxKind.ExclamationToken, operand);
32533257
}
@@ -4593,7 +4597,7 @@ namespace ts {
45934597
const binaryOperatorPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, binaryOperator);
45944598
const binaryOperatorAssociativity = getOperatorAssociativity(SyntaxKind.BinaryExpression, binaryOperator);
45954599
const emittedOperand = skipPartiallyEmittedExpressions(operand);
4596-
if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 4) {
4600+
if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 3) {
45974601
// We need to parenthesize arrow functions on the right side to avoid it being
45984602
// parsed as parenthesized expression: `a && (() => {})`
45994603
return true;

src/compiler/parser.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3009,6 +3009,10 @@ namespace ts {
30093009
return parseJSDocAllType(/*postfixEquals*/ false);
30103010
case SyntaxKind.AsteriskEqualsToken:
30113011
return parseJSDocAllType(/*postfixEquals*/ true);
3012+
case SyntaxKind.QuestionQuestionToken:
3013+
// If there is '??', consider that is prefix '?' in JSDoc type.
3014+
scanner.reScanQuestionToken();
3015+
// falls through
30123016
case SyntaxKind.QuestionToken:
30133017
return parseJSDocUnknownOrNullableType();
30143018
case SyntaxKind.FunctionKeyword:
@@ -4800,6 +4804,7 @@ namespace ts {
48004804
case SyntaxKind.ExclamationEqualsEqualsToken: // foo<x> !==
48014805
case SyntaxKind.AmpersandAmpersandToken: // foo<x> &&
48024806
case SyntaxKind.BarBarToken: // foo<x> ||
4807+
case SyntaxKind.QuestionQuestionToken: // foo<x> ??
48034808
case SyntaxKind.CaretToken: // foo<x> ^
48044809
case SyntaxKind.AmpersandToken: // foo<x> &
48054810
case SyntaxKind.BarToken: // foo<x> |

src/compiler/scanner.ts

+13
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace ts {
3333
scanJsxAttributeValue(): SyntaxKind;
3434
reScanJsxToken(): JsxTokenSyntaxKind;
3535
reScanLessThanToken(): SyntaxKind;
36+
reScanQuestionToken(): SyntaxKind;
3637
scanJsxToken(): JsxTokenSyntaxKind;
3738
scanJsDocToken(): JSDocSyntaxKind;
3839
scan(): SyntaxKind;
@@ -184,6 +185,7 @@ namespace ts {
184185
"&&": SyntaxKind.AmpersandAmpersandToken,
185186
"||": SyntaxKind.BarBarToken,
186187
"?": SyntaxKind.QuestionToken,
188+
"??": SyntaxKind.QuestionQuestionToken,
187189
"?.": SyntaxKind.QuestionDotToken,
188190
":": SyntaxKind.ColonToken,
189191
"=": SyntaxKind.EqualsToken,
@@ -902,6 +904,7 @@ namespace ts {
902904
scanJsxAttributeValue,
903905
reScanJsxToken,
904906
reScanLessThanToken,
907+
reScanQuestionToken,
905908
scanJsxToken,
906909
scanJsDocToken,
907910
scan,
@@ -1834,6 +1837,10 @@ namespace ts {
18341837
pos++;
18351838
return token = SyntaxKind.QuestionDotToken;
18361839
}
1840+
if (text.charCodeAt(pos) === CharacterCodes.question) {
1841+
pos++;
1842+
return token = SyntaxKind.QuestionQuestionToken;
1843+
}
18371844
return token = SyntaxKind.QuestionToken;
18381845
case CharacterCodes.openBracket:
18391846
pos++;
@@ -2023,6 +2030,12 @@ namespace ts {
20232030
return token;
20242031
}
20252032

2033+
function reScanQuestionToken(): SyntaxKind {
2034+
Debug.assert(token === SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'");
2035+
pos = tokenPos + 1;
2036+
return token = SyntaxKind.QuestionToken;
2037+
}
2038+
20262039
function scanJsxToken(): JsxTokenSyntaxKind {
20272040
startPos = tokenPos = pos;
20282041

src/compiler/transformers/esnext.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
namespace ts {
33
export function transformESNext(context: TransformationContext) {
44
const {
5-
hoistVariableDeclaration
5+
hoistVariableDeclaration,
66
} = context;
77

88
return chainBundle(transformSourceFile);
@@ -28,7 +28,12 @@ namespace ts {
2828
Debug.assertNotNode(updated, isSyntheticReference);
2929
return updated;
3030
}
31-
// falls through
31+
return visitEachChild(node, visitor, context);
32+
case SyntaxKind.BinaryExpression:
33+
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) {
34+
return transformNullishCoalescingExpression(<BinaryExpression>node);
35+
}
36+
return visitEachChild(node, visitor, context);
3237
default:
3338
return visitEachChild(node, visitor, context);
3439
}
@@ -172,5 +177,39 @@ namespace ts {
172177
);
173178
return thisArg ? createSyntheticReferenceExpression(target, thisArg) : target;
174179
}
180+
181+
function createNotNullCondition(node: Expression) {
182+
return createBinary(
183+
createBinary(
184+
node,
185+
createToken(SyntaxKind.ExclamationEqualsEqualsToken),
186+
createNull()
187+
),
188+
createToken(SyntaxKind.AmpersandAmpersandToken),
189+
createBinary(
190+
node,
191+
createToken(SyntaxKind.ExclamationEqualsEqualsToken),
192+
createVoidZero()
193+
)
194+
);
195+
}
196+
197+
function transformNullishCoalescingExpression(node: BinaryExpression) {
198+
const expressions: Expression[] = [];
199+
let left = visitNode(node.left, visitor, isExpression);
200+
if (!isIdentifier(left)) {
201+
const temp = createTempVariable(hoistVariableDeclaration);
202+
expressions.push(createAssignment(temp, left));
203+
left = temp;
204+
}
205+
expressions.push(
206+
createParen(
207+
createConditional(
208+
createNotNullCondition(left),
209+
left,
210+
visitNode(node.right, visitor, isExpression)))
211+
);
212+
return inlineExpressions(expressions);
213+
}
175214
}
176215
}

src/compiler/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ namespace ts {
184184
QuestionToken,
185185
ColonToken,
186186
AtToken,
187+
QuestionQuestionToken,
187188
/** Only the JSDoc scanner produces BacktickToken. The normal scanner produces NoSubstitutionTemplateLiteral and related kinds. */
188189
BacktickToken,
189190
// Assignments
@@ -1566,7 +1567,8 @@ namespace ts {
15661567

15671568
// see: https://tc39.github.io/ecma262/#prod-AssignmentExpression
15681569
export type AssignmentOperatorOrHigher
1569-
= LogicalOperatorOrHigher
1570+
= SyntaxKind.QuestionQuestionToken
1571+
| LogicalOperatorOrHigher
15701572
| AssignmentOperator
15711573
;
15721574

0 commit comments

Comments
 (0)