Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nullish coalescing commit #32883

Merged
merged 34 commits into from Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2ef3e0f
migrate nullish coalescing commit
Kingwl Aug 2, 2019
91a04ed
add more test case
Kingwl Aug 14, 2019
ba39ae8
add branch type check test
Kingwl Aug 14, 2019
079d002
add more tests
Kingwl Aug 14, 2019
3822fde
fix nullish precedence
Kingwl Aug 14, 2019
f67e524
update public api
Kingwl Aug 14, 2019
2327b3b
add rescan question question token to fix regression
Kingwl Aug 15, 2019
fb0d3c9
update public api baseline
Kingwl Aug 15, 2019
06149d0
Added tests that emit for nullish coalescing operator conforming with…
dragomirtitian Aug 15, 2019
3d94652
Fixed emit to hoist temporary variables (they previously went undecla…
dragomirtitian Aug 15, 2019
65b2008
Merge branch 'nullish-coalescing-operator' of https://github.com/King…
dragomirtitian Aug 15, 2019
e4497b8
Merge pull request #1 from dragomirtitian/nullish-coalescing-operator
Kingwl Aug 16, 2019
c2e336b
use not equal to null
Kingwl Aug 16, 2019
b714a01
rename factory
Kingwl Aug 16, 2019
323ebf0
add grammar check
Kingwl Aug 16, 2019
d659c83
fix more cases
Kingwl Aug 16, 2019
55dd6ae
Fix handling of nullish coalescing oprator in expando objects.
dragomirtitian Aug 18, 2019
22d1c07
Fixed classifier to support ?? operator.
dragomirtitian Aug 18, 2019
8427eca
Merge pull request #2 from dragomirtitian/nullish-coalescing-operator
Kingwl Aug 19, 2019
cb76a3e
Merge branch 'master' into nullish-coalescing-operator
Aug 19, 2019
7d3ccae
update baseline
Kingwl Aug 20, 2019
b412e50
accept baseline
Kingwl Aug 20, 2019
8227034
Merge branch 'master' into nullish-coalescing-operator
Kingwl Sep 13, 2019
57be95d
fix review
Kingwl Sep 13, 2019
78df31b
update baseline
Kingwl Sep 27, 2019
9e3852e
Merge branch 'nullish-coalescing-operator' of github.com:Kingwl/TypeS…
Kingwl Sep 27, 2019
de81520
update emitter and more testcase
Kingwl Sep 27, 2019
6418943
update control flow
Kingwl Sep 27, 2019
a719f66
make linter happy
Kingwl Sep 27, 2019
cb187cb
update libs
Kingwl Sep 27, 2019
7fd8658
avoid unnecessary assert
Kingwl Sep 27, 2019
6ef160f
fix typooo
Kingwl Sep 27, 2019
8c572b1
Merge branch 'master' into nullish-coalescing-operator
rbuckton Sep 30, 2019
b030f73
Fixes for control-flow analysis
rbuckton Sep 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/compiler/binder.ts
Expand Up @@ -1435,6 +1435,14 @@ namespace ts {
bindCondition(node.right, trueTarget, falseTarget);
}

function bindNullishCollasingExpression(node: BinaryExpression, trueTarget: FlowLabel, falseTarget: FlowLabel) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: bindNullishCollasingExpression -> bindNullishCoalescingExpression

const notNullLabel = createBranchLabel();
bindCondition(node.left, trueTarget, notNullLabel);
currentFlow = finishFlowLabel(notNullLabel);
bind(node.operatorToken);
bindCondition(node.right, trueTarget, falseTarget!);
}

function bindPrefixUnaryExpressionFlow(node: PrefixUnaryExpression) {
if (node.operator === SyntaxKind.ExclamationToken) {
const saveTrueTarget = currentTrueTarget;
Expand All @@ -1461,12 +1469,15 @@ namespace ts {

function bindBinaryExpressionFlow(node: BinaryExpression) {
const operator = node.operatorToken.kind;
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken) {
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This if-chain would be easier to read as a switch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep this because of so many other code similar to that.

if (isTopLevelLogicalExpression(node)) {
const postExpressionLabel = createBranchLabel();
bindLogicalExpression(node, postExpressionLabel, postExpressionLabel);
currentFlow = finishFlowLabel(postExpressionLabel);
}
else if (operator === SyntaxKind.QuestionQuestionToken) {
bindNullishCollasingExpression(node, currentTrueTarget!, currentFalseTarget!);
}
else {
bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!);
}
Expand Down Expand Up @@ -2813,7 +2824,7 @@ namespace ts {
init = init && getRightMostAssignedExpression(init);
if (init) {
const isPrototypeAssignment = isPrototypeAccess(isVariableDeclaration(node) ? node.name : isBinaryExpression(node) ? node.left : node);
return !!getExpandoInitializer(isBinaryExpression(init) && init.operatorToken.kind === SyntaxKind.BarBarToken ? init.right : init, isPrototypeAssignment);
return !!getExpandoInitializer(isBinaryExpression(init) && (init.operatorToken.kind === SyntaxKind.BarBarToken || init.operatorToken.kind === SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment);
}
return false;
}
Expand Down Expand Up @@ -3285,7 +3296,10 @@ namespace ts {
const operatorTokenKind = node.operatorToken.kind;
const leftKind = node.left.kind;

if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) {
if (operatorTokenKind === SyntaxKind.QuestionQuestionToken) {
transformFlags |= TransformFlags.AssertESNext;
}
else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) {
DanielRosenwasser marked this conversation as resolved.
Show resolved Hide resolved
// Destructuring object assignments with are ES2015 syntax
// and possibly ES2018 if they contain rest
transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment;
Expand Down
22 changes: 20 additions & 2 deletions src/compiler/checker.ts
Expand Up @@ -13140,7 +13140,7 @@ namespace ts {
return isContextSensitive((<ConditionalExpression>node).whenTrue) ||
isContextSensitive((<ConditionalExpression>node).whenFalse);
case SyntaxKind.BinaryExpression:
return (<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken &&
return ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.BarBarToken || (<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) &&
(isContextSensitive((<BinaryExpression>node).left) || isContextSensitive((<BinaryExpression>node).right));
case SyntaxKind.PropertyAssignment:
return isContextSensitive((<PropertyAssignment>node).initializer);
Expand Down Expand Up @@ -20710,6 +20710,7 @@ namespace ts {
}
return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive;
case SyntaxKind.BarBarToken:
case SyntaxKind.QuestionQuestionToken:
// When an || expression has a contextual type, the operands are contextually typed by that type, except
// when that type originates in a binding pattern, the right operand is contextually typed by the type of
// the left operand. When an || expression has no contextual type, the right operand is contextually typed
Expand Down Expand Up @@ -26247,16 +26248,29 @@ namespace ts {
if (isInJSFile(node) && getAssignedExpandoInitializer(node)) {
return checkExpression(node.right, checkMode);
}
checkGrammarNullishCoalesceWithLogicalExpression(node);
return checkBinaryLikeExpression(node.left, node.operatorToken, node.right, checkMode, node);
}

function checkGrammarNullishCoalesceWithLogicalExpression (node: BinaryExpression) {
DanielRosenwasser marked this conversation as resolved.
Show resolved Hide resolved
const { left, operatorToken, right } = node;
if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) {
if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind));
}
if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind));
}
}
}

function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): Type {
const operator = operatorToken.kind;
if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) {
return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === SyntaxKind.ThisKeyword);
}
let leftType: Type;
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken) {
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) {
leftType = checkTruthinessExpression(left, checkMode);
}
else {
Expand Down Expand Up @@ -26417,6 +26431,10 @@ namespace ts {
return getTypeFacts(leftType) & TypeFacts.Falsy ?
getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], UnionReduction.Subtype) :
leftType;
case SyntaxKind.QuestionQuestionToken:
return getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ?
getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) :
leftType;
case SyntaxKind.EqualsToken:
const declKind = isBinaryExpression(left.parent) ? getAssignmentDeclarationKind(left.parent) : AssignmentDeclarationKind.None;
checkAssignmentDeclaration(declKind, rightType);
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -3229,6 +3229,10 @@
"category": "Error",
"code": 5075
},
"'{0}' and '{1}' operations cannot be mixed without parentheses.": {
"category": "Error",
"code": 5076
},

"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/factory.ts
Expand Up @@ -3170,6 +3170,10 @@ namespace ts {
return createBinary(left, SyntaxKind.BarBarToken, right);
}

export function createNullishCoalesce(left: Expression, right: Expression) {
return createBinary(left, SyntaxKind.QuestionQuestionToken, right);
}

export function createLogicalNot(operand: Expression) {
return createPrefix(SyntaxKind.ExclamationToken, operand);
}
Expand Down Expand Up @@ -4515,7 +4519,7 @@ namespace ts {
const binaryOperatorPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, binaryOperator);
const binaryOperatorAssociativity = getOperatorAssociativity(SyntaxKind.BinaryExpression, binaryOperator);
const emittedOperand = skipPartiallyEmittedExpressions(operand);
if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 4) {
if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 3) {
// We need to parenthesize arrow functions on the right side to avoid it being
// parsed as parenthesized expression: `a && (() => {})`
return true;
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/parser.ts
Expand Up @@ -3005,6 +3005,10 @@ namespace ts {
return parseJSDocAllType(/*postfixEquals*/ false);
case SyntaxKind.AsteriskEqualsToken:
return parseJSDocAllType(/*postfixEquals*/ true);
case SyntaxKind.QuestionQuestionToken:
// If there is '??', consider that is prefix '?' in JSDoc type.
scanner.reScanQuestionToken();
// falls through
case SyntaxKind.QuestionToken:
return parseJSDocUnknownOrNullableType();
case SyntaxKind.FunctionKeyword:
Expand Down Expand Up @@ -4737,6 +4741,7 @@ namespace ts {
case SyntaxKind.ExclamationEqualsEqualsToken: // foo<x> !==
case SyntaxKind.AmpersandAmpersandToken: // foo<x> &&
case SyntaxKind.BarBarToken: // foo<x> ||
case SyntaxKind.QuestionQuestionToken: // foo<x> ??
case SyntaxKind.CaretToken: // foo<x> ^
case SyntaxKind.AmpersandToken: // foo<x> &
case SyntaxKind.BarToken: // foo<x> |
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/scanner.ts
Expand Up @@ -33,6 +33,7 @@ namespace ts {
scanJsxAttributeValue(): SyntaxKind;
reScanJsxToken(): JsxTokenSyntaxKind;
reScanLessThanToken(): SyntaxKind;
reScanQuestionToken(): SyntaxKind;
scanJsxToken(): JsxTokenSyntaxKind;
scanJsDocToken(): JSDocSyntaxKind;
scan(): SyntaxKind;
Expand Down Expand Up @@ -184,6 +185,7 @@ namespace ts {
"&&": SyntaxKind.AmpersandAmpersandToken,
"||": SyntaxKind.BarBarToken,
"?": SyntaxKind.QuestionToken,
"??": SyntaxKind.QuestionQuestionToken,
":": SyntaxKind.ColonToken,
"=": SyntaxKind.EqualsToken,
"+=": SyntaxKind.PlusEqualsToken,
Expand Down Expand Up @@ -901,6 +903,7 @@ namespace ts {
scanJsxAttributeValue,
reScanJsxToken,
reScanLessThanToken,
reScanQuestionToken,
scanJsxToken,
scanJsDocToken,
scan,
Expand Down Expand Up @@ -1828,6 +1831,9 @@ namespace ts {
pos++;
return token = SyntaxKind.GreaterThanToken;
case CharacterCodes.question:
if (text.charCodeAt(pos + 1) === CharacterCodes.question) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If you re-order the pos++ before the if, the code become marginally simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to be consistent with the existed code.

return pos += 2, token = SyntaxKind.QuestionQuestionToken;
}
pos++;
return token = SyntaxKind.QuestionToken;
case CharacterCodes.openBracket:
Expand Down Expand Up @@ -2018,6 +2024,12 @@ namespace ts {
return token;
}

function reScanQuestionToken(): SyntaxKind {
Debug.assert(token === SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'");
pos = tokenPos + 1;
return token = SyntaxKind.QuestionToken;
}

function scanJsxToken(): JsxTokenSyntaxKind {
startPos = tokenPos = pos;

Expand Down
43 changes: 43 additions & 0 deletions src/compiler/transformers/esnext.ts
@@ -1,6 +1,10 @@
/*@internal*/
namespace ts {
export function transformESNext(context: TransformationContext) {
const {
hoistVariableDeclaration,
} = context;

return chainBundle(transformSourceFile);

function transformSourceFile(node: SourceFile) {
Expand All @@ -16,9 +20,48 @@ namespace ts {
return node;
}
switch (node.kind) {
case SyntaxKind.BinaryExpression:
if ((<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) {
return transformNullishCoalescingExpression(<BinaryExpression>node);
}
// falls through
default:
return visitEachChild(node, visitor, context);
}
}

function createNotNullCondition(node: Expression) {
return createBinary(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since optional chaining uses strict equality, can you switch this to strict equality?

createBinary(
    createBinary(
        temp
        createToken(SyntaxKind.ExclamationEqualsEqualsToken),
        createNull()
    ),
    createToken(SyntaxKind.AmpersandAmpersandToken),
    createBinary(
        temp,
        createToken(SyntaxKind.ExclamationEqualsEqualsToken),
        createVoidZero()
    )
);

(@rbuckton)

createBinary(
node,
createToken(SyntaxKind.ExclamationEqualsEqualsToken),
createNull()
),
createToken(SyntaxKind.AmpersandAmpersandToken),
createBinary(
node,
createToken(SyntaxKind.ExclamationEqualsEqualsToken),
createVoidZero()
)
);
}

function transformNullishCoalescingExpression(node: BinaryExpression) {
const expressions: Expression[] = [];
let left = visitNode(node.left, visitor, isExpression);
if (!isIdentifier(left)) {
const temp = createTempVariable(hoistVariableDeclaration);
expressions.push(createAssignment(temp, left));
left = temp;
}
expressions.push(
createParen(
createConditional(
createNotNullCondition(left),
left,
visitNode(node.right, visitor, isExpression)))
);
return inlineExpressions(expressions);
}
}
}
4 changes: 3 additions & 1 deletion src/compiler/types.ts
Expand Up @@ -183,6 +183,7 @@ namespace ts {
QuestionToken,
ColonToken,
AtToken,
QuestionQuestionToken,
/** Only the JSDoc scanner produces BacktickToken. The normal scanner produces NoSubstitutionTemplateLiteral and related kinds. */
BacktickToken,
// Assignments
Expand Down Expand Up @@ -1546,7 +1547,8 @@ namespace ts {

// see: https://tc39.github.io/ecma262/#prod-AssignmentExpression
export type AssignmentOperatorOrHigher
= LogicalOperatorOrHigher
= SyntaxKind.QuestionQuestionToken
| LogicalOperatorOrHigher
| AssignmentOperator
;

Expand Down