Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40548,12 +40548,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
checkNullishCoalesceOperandLeft(node);
checkNullishCoalesceOperandRight(node);
}

function checkNullishCoalesceOperandLeft(node: BinaryExpression) {
const leftTarget = skipOuterExpressions(node.left, OuterExpressionKinds.All);

const nullishSemantics = getSyntacticNullishnessSemantics(leftTarget);
if (nullishSemantics !== PredicateSemantics.Sometimes) {
if (nullishSemantics === PredicateSemantics.Always) {
Expand All @@ -40565,25 +40563,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function checkNullishCoalesceOperandRight(node: BinaryExpression) {
const rightTarget = skipOuterExpressions(node.right, OuterExpressionKinds.All);
const nullishSemantics = getSyntacticNullishnessSemantics(rightTarget);
if (isNotWithinNullishCoalesceExpression(node)) {
return;
}

if (nullishSemantics === PredicateSemantics.Always) {
error(rightTarget, Diagnostics.This_expression_is_always_nullish);
}
else if (nullishSemantics === PredicateSemantics.Never) {
error(rightTarget, Diagnostics.This_expression_is_never_nullish);
}
}

function isNotWithinNullishCoalesceExpression(node: BinaryExpression) {
return !isBinaryExpression(node.parent) || node.parent.operatorToken.kind !== SyntaxKind.QuestionQuestionToken;
}

function getSyntacticNullishnessSemantics(node: Node): PredicateSemantics {
node = skipOuterExpressions(node);
switch (node.kind) {
Expand All @@ -40601,15 +40580,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// List of operators that can produce null/undefined:
// = ??= ?? || ||= && &&=
switch ((node as BinaryExpression).operatorToken.kind) {
case SyntaxKind.EqualsToken:
case SyntaxKind.QuestionQuestionToken:
case SyntaxKind.QuestionQuestionEqualsToken:
case SyntaxKind.BarBarToken:
case SyntaxKind.BarBarEqualsToken:
case SyntaxKind.AmpersandAmpersandToken:
case SyntaxKind.AmpersandAmpersandEqualsToken:
return PredicateSemantics.Sometimes;
// For these operator kinds, the right operand is effectively controlling
case SyntaxKind.CommaToken:
case SyntaxKind.EqualsToken:
case SyntaxKind.QuestionQuestionToken:
case SyntaxKind.QuestionQuestionEqualsToken:
return getSyntacticNullishnessSemantics((node as BinaryExpression).right);
}
return PredicateSemantics.Never;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
neverNullishThroughParentheses.ts(6,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(7,14): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(10,15): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(11,16): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(14,15): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(15,16): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(16,17): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(16,17): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.


==== neverNullishThroughParentheses.ts (8 errors) ====
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses

const x: { y: string | undefined } | undefined = undefined as any;

// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const bar = (x?.y ?? `oops`) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.

// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const qux = (((x?.y ?? `oops`))) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.

// Test with different types
const str1 = ("literal") ?? "fallback";
~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const str2 = (("nested")) ?? "fallback";
~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const nested = ("a" ?? "b") ?? "c";
~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.

36 changes: 36 additions & 0 deletions tests/baselines/reference/neverNullishThroughParentheses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////

//// [neverNullishThroughParentheses.ts]
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses

const x: { y: string | undefined } | undefined = undefined as any;

// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
const bar = (x?.y ?? `oops`) ?? "";

// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
const qux = (((x?.y ?? `oops`))) ?? "";

// Test with different types
const str1 = ("literal") ?? "fallback";
const str2 = (("nested")) ?? "fallback";
const nested = ("a" ?? "b") ?? "c";


//// [neverNullishThroughParentheses.js]
"use strict";
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
var x = undefined;
// Both should error - both expressions are guaranteed to be "oops"
var foo = (_b = (_a = x === null || x === void 0 ? void 0 : x.y) !== null && _a !== void 0 ? _a : "oops") !== null && _b !== void 0 ? _b : "";
var bar = (_d = ((_c = x === null || x === void 0 ? void 0 : x.y) !== null && _c !== void 0 ? _c : "oops")) !== null && _d !== void 0 ? _d : "";
// Additional test cases with various levels of nesting
var baz = (_f = (((_e = x === null || x === void 0 ? void 0 : x.y) !== null && _e !== void 0 ? _e : "oops"))) !== null && _f !== void 0 ? _f : "";
var qux = (_h = ((((_g = x === null || x === void 0 ? void 0 : x.y) !== null && _g !== void 0 ? _g : "oops")))) !== null && _h !== void 0 ? _h : "";
// Test with different types
var str1 = (_j = ("literal")) !== null && _j !== void 0 ? _j : "fallback";
var str2 = (_k = (("nested"))) !== null && _k !== void 0 ? _k : "fallback";
var nested = (_l = ("a" !== null && "a" !== void 0 ? "a" : "b")) !== null && _l !== void 0 ? _l : "c";
46 changes: 46 additions & 0 deletions tests/baselines/reference/neverNullishThroughParentheses.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////

=== neverNullishThroughParentheses.ts ===
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses

const x: { y: string | undefined } | undefined = undefined as any;
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>undefined : Symbol(undefined)

// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
>foo : Symbol(foo, Decl(neverNullishThroughParentheses.ts, 5, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))

const bar = (x?.y ?? `oops`) ?? "";
>bar : Symbol(bar, Decl(neverNullishThroughParentheses.ts, 6, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))

// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
>baz : Symbol(baz, Decl(neverNullishThroughParentheses.ts, 9, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))

const qux = (((x?.y ?? `oops`))) ?? "";
>qux : Symbol(qux, Decl(neverNullishThroughParentheses.ts, 10, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))

// Test with different types
const str1 = ("literal") ?? "fallback";
>str1 : Symbol(str1, Decl(neverNullishThroughParentheses.ts, 13, 5))

const str2 = (("nested")) ?? "fallback";
>str2 : Symbol(str2, Decl(neverNullishThroughParentheses.ts, 14, 5))

const nested = ("a" ?? "b") ?? "c";
>nested : Symbol(nested, Decl(neverNullishThroughParentheses.ts, 15, 5))

144 changes: 144 additions & 0 deletions tests/baselines/reference/neverNullishThroughParentheses.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////

=== neverNullishThroughParentheses.ts ===
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses

const x: { y: string | undefined } | undefined = undefined as any;
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>undefined as any : any
> : ^^^
>undefined : undefined
> : ^^^^^^^^^

// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
>foo : string
> : ^^^^^^
>x?.y ?? `oops` ?? "" : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^

const bar = (x?.y ?? `oops`) ?? "";
>bar : string
> : ^^^^^^
>(x?.y ?? `oops`) ?? "" : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^

// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
>baz : string
> : ^^^^^^
>((x?.y ?? `oops`)) ?? "" : string
> : ^^^^^^
>((x?.y ?? `oops`)) : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^

const qux = (((x?.y ?? `oops`))) ?? "";
>qux : string
> : ^^^^^^
>(((x?.y ?? `oops`))) ?? "" : string
> : ^^^^^^
>(((x?.y ?? `oops`))) : string
> : ^^^^^^
>((x?.y ?? `oops`)) : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^

// Test with different types
const str1 = ("literal") ?? "fallback";
>str1 : "literal"
> : ^^^^^^^^^
>("literal") ?? "fallback" : "literal"
> : ^^^^^^^^^
>("literal") : "literal"
> : ^^^^^^^^^
>"literal" : "literal"
> : ^^^^^^^^^
>"fallback" : "fallback"
> : ^^^^^^^^^^

const str2 = (("nested")) ?? "fallback";
>str2 : "nested"
> : ^^^^^^^^
>(("nested")) ?? "fallback" : "nested"
> : ^^^^^^^^
>(("nested")) : "nested"
> : ^^^^^^^^
>("nested") : "nested"
> : ^^^^^^^^
>"nested" : "nested"
> : ^^^^^^^^
>"fallback" : "fallback"
> : ^^^^^^^^^^

const nested = ("a" ?? "b") ?? "c";
>nested : "a"
> : ^^^
>("a" ?? "b") ?? "c" : "a"
> : ^^^
>("a" ?? "b") : "a"
> : ^^^
>"a" ?? "b" : "a"
> : ^^^
>"a" : "a"
> : ^^^
>"b" : "b"
> : ^^^
>"c" : "c"
> : ^^^

Loading