Fixed nullish coalesce operator's precedence (#61372)

This commit is contained in:
Mateusz Burzyński 2025-06-30 22:49:03 +02:00 committed by GitHub
parent 58665cf3ae
commit 91cdbd518c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 240 additions and 30 deletions

View File

@ -40472,18 +40472,29 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function checkNullishCoalesceOperands(node: BinaryExpression) {
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));
}
checkNullishCoalesceOperandLeft(node);
checkNullishCoalesceOperandRight(node);
if (node.operatorToken.kind !== SyntaxKind.QuestionQuestionToken) {
return;
}
if (isBinaryExpression(node.parent)) {
const { left, operatorToken } = node.parent;
if (isBinaryExpression(left) && operatorToken.kind === SyntaxKind.BarBarToken) {
grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(SyntaxKind.QuestionQuestionToken), tokenToString(operatorToken.kind));
}
}
else if (isBinaryExpression(node.left)) {
const { operatorToken } = node.left;
if (operatorToken.kind === SyntaxKind.BarBarToken || operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
grammarErrorOnNode(node.left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(operatorToken.kind), tokenToString(SyntaxKind.QuestionQuestionToken));
}
}
else if (isBinaryExpression(node.right)) {
const { operatorToken } = node.right;
if (operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) {
grammarErrorOnNode(node.right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(SyntaxKind.QuestionQuestionToken), tokenToString(operatorToken.kind));
}
}
checkNullishCoalesceOperandLeft(node);
checkNullishCoalesceOperandRight(node);
}
function checkNullishCoalesceOperandLeft(node: BinaryExpression) {

View File

@ -5675,17 +5675,17 @@ export const enum OperatorPrecedence {
// CoalesceExpression
Conditional,
// LogicalORExpression:
// LogicalANDExpression
// LogicalORExpression `||` LogicalANDExpression
LogicalOR,
// CoalesceExpression:
// CoalesceExpressionHead `??` BitwiseORExpression
// CoalesceExpressionHead:
// CoalesceExpression
// BitwiseORExpression
Coalesce = Conditional, // NOTE: This is wrong
// LogicalORExpression:
// LogicalANDExpression
// LogicalORExpression `||` LogicalANDExpression
LogicalOR,
Coalesce = LogicalOR,
// LogicalANDExpression:
// BitwiseORExpression

View File

@ -366,5 +366,62 @@ describe("unittests:: PrinterAPI", () => {
ts.factory.createNoSubstitutionTemplateLiteral("\n"),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryBarBarExpressionWithLeftConditionalExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createConditionalExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.ColonToken),
ts.factory.createIdentifier("c"),
),
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
ts.factory.createIdentifier("d"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryAmpersandAmpersandExpressionWithLeftConditionalExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createConditionalExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.ColonToken),
ts.factory.createIdentifier("c"),
),
ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.factory.createIdentifier("d"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
printsCorrectly("binaryQuestionQuestionExpressionWithLeftConditionalExpression", {}, printer =>
printer.printNode(
ts.EmitHint.Unspecified,
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createConditionalExpression(
ts.factory.createIdentifier("a"),
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
ts.factory.createIdentifier("b"),
ts.factory.createToken(ts.SyntaxKind.ColonToken),
ts.factory.createIdentifier("c"),
),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createIdentifier("d"),
),
),
ts.createSourceFile("source.ts", "", ts.ScriptTarget.ESNext),
));
});
});

View File

@ -0,0 +1,45 @@
//// [tests/cases/conformance/expressions/nullishCoalescingOperator/nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts] ////
//// [nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts]
// https://github.com/microsoft/TypeScript/issues/61109
class Cls {
#privateProp: number | undefined;
problem() {
this.#privateProp ??= false ? neverThis() : 20;
}
}
function neverThis(): never {
throw new Error("This should really really never happen!");
}
//// [nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.js]
"use strict";
// https://github.com/microsoft/TypeScript/issues/61109
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Cls_privateProp;
class Cls {
constructor() {
_Cls_privateProp.set(this, void 0);
}
problem() {
__classPrivateFieldSet(this, _Cls_privateProp, __classPrivateFieldGet(this, _Cls_privateProp, "f") ?? (false ? neverThis() : 20), "f");
}
}
_Cls_privateProp = new WeakMap();
function neverThis() {
throw new Error("This should really really never happen!");
}

View File

@ -0,0 +1,28 @@
//// [tests/cases/conformance/expressions/nullishCoalescingOperator/nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts] ////
=== nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts ===
// https://github.com/microsoft/TypeScript/issues/61109
class Cls {
>Cls : Symbol(Cls, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 0, 0))
#privateProp: number | undefined;
>#privateProp : Symbol(Cls.#privateProp, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 2, 11))
problem() {
>problem : Symbol(Cls.problem, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 3, 35))
this.#privateProp ??= false ? neverThis() : 20;
>this.#privateProp : Symbol(Cls.#privateProp, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 2, 11))
>this : Symbol(Cls, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 0, 0))
>neverThis : Symbol(neverThis, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 8, 1))
}
}
function neverThis(): never {
>neverThis : Symbol(neverThis, Decl(nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts, 8, 1))
throw new Error("This should really really never happen!");
>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
}

View File

@ -0,0 +1,50 @@
//// [tests/cases/conformance/expressions/nullishCoalescingOperator/nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts] ////
=== nullishCoalescingAssignmentVsPrivateFieldsJsEmit1.ts ===
// https://github.com/microsoft/TypeScript/issues/61109
class Cls {
>Cls : Cls
> : ^^^
#privateProp: number | undefined;
>#privateProp : number | undefined
> : ^^^^^^^^^^^^^^^^^^
problem() {
>problem : () => void
> : ^^^^^^^^^^
this.#privateProp ??= false ? neverThis() : 20;
>this.#privateProp ??= false ? neverThis() : 20 : number
> : ^^^^^^
>this.#privateProp : number | undefined
> : ^^^^^^^^^^^^^^^^^^
>this : this
> : ^^^^
>false ? neverThis() : 20 : 20
> : ^^
>false : false
> : ^^^^^
>neverThis() : never
> : ^^^^^
>neverThis : () => never
> : ^^^^^^
>20 : 20
> : ^^
}
}
function neverThis(): never {
>neverThis : () => never
> : ^^^^^^
throw new Error("This should really really never happen!");
>new Error("This should really really never happen!") : Error
> : ^^^^^
>Error : ErrorConstructor
> : ^^^^^^^^^^^^^^^^
>"This should really really never happen!" : "This should really really never happen!"
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

View File

@ -1,6 +1,6 @@
nullishCoalescingOperator5.ts(6,6): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
nullishCoalescingOperator5.ts(6,1): error TS5076: '??' and '||' operations cannot be mixed without parentheses.
nullishCoalescingOperator5.ts(9,1): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
nullishCoalescingOperator5.ts(12,6): error TS5076: '&&' and '??' operations cannot be mixed without parentheses.
nullishCoalescingOperator5.ts(12,6): error TS5076: '??' and '&&' operations cannot be mixed without parentheses.
nullishCoalescingOperator5.ts(15,1): error TS5076: '&&' and '??' operations cannot be mixed without parentheses.
@ -11,8 +11,8 @@ nullishCoalescingOperator5.ts(15,1): error TS5076: '&&' and '??' operations cann
// should be a syntax error
a ?? b || c;
~~~~~~
!!! error TS5076: '||' and '??' operations cannot be mixed without parentheses.
~~~~~~
!!! error TS5076: '??' and '||' operations cannot be mixed without parentheses.
// should be a syntax error
a || b ?? c;
@ -22,7 +22,7 @@ nullishCoalescingOperator5.ts(15,1): error TS5076: '&&' and '??' operations cann
// should be a syntax error
a ?? b && c;
~~~~~~
!!! error TS5076: '&&' and '??' operations cannot be mixed without parentheses.
!!! error TS5076: '??' and '&&' operations cannot be mixed without parentheses.
// should be a syntax error
a && b ?? c;

View File

@ -46,7 +46,7 @@ a && (b ?? c);
"use strict";
var _a, _b, _c, _d;
// should be a syntax error
a !== null && a !== void 0 ? a : b || c;
(a !== null && a !== void 0 ? a : b) || c;
// should be a syntax error
(_a = a || b) !== null && _a !== void 0 ? _a : c;
// should be a syntax error

View File

@ -17,10 +17,10 @@ declare const c: string | undefined
a ?? b || c;
>a ?? b || c : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>a ?? b : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>a : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>b || c : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>b : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>c : string | undefined

View File

@ -59,7 +59,7 @@ plainJSGrammarErrors.js(92,33): error TS2566: A rest element cannot have a prope
plainJSGrammarErrors.js(93,42): error TS1186: A rest element cannot have an initializer.
plainJSGrammarErrors.js(96,4): error TS1123: Variable declaration list cannot be empty.
plainJSGrammarErrors.js(97,9): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
plainJSGrammarErrors.js(98,14): error TS5076: '||' and '??' operations cannot be mixed without parentheses.
plainJSGrammarErrors.js(98,9): error TS5076: '??' and '||' operations cannot be mixed without parentheses.
plainJSGrammarErrors.js(100,3): error TS1200: Line terminator not permitted before arrow.
plainJSGrammarErrors.js(102,4): error TS1358: Tagged template expressions are not permitted in an optional chain.
plainJSGrammarErrors.js(104,6): error TS1171: A comma expression is not allowed in a computed property name.
@ -323,8 +323,8 @@ plainJSGrammarErrors.js(205,36): error TS1325: Argument of dynamic import cannot
~~~~~~
!!! error TS5076: '||' and '??' operations cannot be mixed without parentheses.
var x = 2 ?? 3 || 4
~~~~~~
!!! error TS5076: '||' and '??' operations cannot be mixed without parentheses.
~~~~~~
!!! error TS5076: '??' and '||' operations cannot be mixed without parentheses.
const arr = x
=> x + 1
~~

View File

@ -421,10 +421,10 @@ var x = 2 ?? 3 || 4
> : ^^^^^^
>2 ?? 3 || 4 : 2 | 3 | 4
> : ^^^^^^^^^
>2 ?? 3 : 2 | 3
> : ^^^^^
>2 : 2
> : ^
>3 || 4 : 3 | 4
> : ^^^^^
>3 : 3
> : ^
>4 : 4

View File

@ -0,0 +1,16 @@
// @strict: true
// @target: es2021
// https://github.com/microsoft/TypeScript/issues/61109
class Cls {
#privateProp: number | undefined;
problem() {
this.#privateProp ??= false ? neverThis() : 20;
}
}
function neverThis(): never {
throw new Error("This should really really never happen!");
}