Skip to content

Commit 71fb864

Browse files
Disallow truthiness/nullishness checks on syntax that never varies on it (#59217)
1 parent 3f2a9e4 commit 71fb864

File tree

77 files changed

+2669
-34
lines changed

Some content is hidden

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

77 files changed

+2669
-34
lines changed

src/compiler/checker.ts

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ import {
470470
isAutoAccessorPropertyDeclaration,
471471
isAwaitExpression,
472472
isBinaryExpression,
473+
isBinaryLogicalOperator,
473474
isBindableObjectDefinePropertyCall,
474475
isBindableStaticElementAccessExpression,
475476
isBindableStaticNameExpression,
@@ -899,6 +900,7 @@ import {
899900
NodeWithTypeArguments,
900901
NonNullChain,
901902
NonNullExpression,
903+
NoSubstitutionTemplateLiteral,
902904
not,
903905
noTruncationMaximumTruncationLength,
904906
NumberLiteralType,
@@ -929,6 +931,7 @@ import {
929931
PatternAmbientModule,
930932
PlusToken,
931933
PostfixUnaryExpression,
934+
PredicateSemantics,
932935
PrefixUnaryExpression,
933936
PrivateIdentifier,
934937
Program,
@@ -39470,7 +39473,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3947039473
return state;
3947139474
}
3947239475

39473-
checkGrammarNullishCoalesceWithLogicalExpression(node);
39476+
checkNullishCoalesceOperands(node);
3947439477

3947539478
const operator = node.operatorToken.kind;
3947639479
if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) {
@@ -39503,7 +39506,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3950339506
if (operator === SyntaxKind.AmpersandAmpersandToken || isIfStatement(parent)) {
3950439507
checkTestingKnownTruthyCallableOrAwaitableOrEnumMemberType(node.left, leftType, isIfStatement(parent) ? parent.thenStatement : undefined);
3950539508
}
39506-
checkTruthinessOfType(leftType, node.left);
39509+
if (isBinaryLogicalOperator(operator)) {
39510+
checkTruthinessOfType(leftType, node.left);
39511+
}
3950739512
}
3950839513
}
3950939514
}
@@ -39569,7 +39574,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3956939574
}
3957039575
}
3957139576

39572-
function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) {
39577+
function checkNullishCoalesceOperands(node: BinaryExpression) {
3957339578
const { left, operatorToken, right } = node;
3957439579
if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) {
3957539580
if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
@@ -39578,7 +39583,60 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3957839583
if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) {
3957939584
grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind));
3958039585
}
39586+
39587+
const leftTarget = skipOuterExpressions(left, OuterExpressionKinds.All);
39588+
const nullishSemantics = getSyntacticNullishnessSemantics(leftTarget);
39589+
if (nullishSemantics !== PredicateSemantics.Sometimes) {
39590+
if (node.parent.kind === SyntaxKind.BinaryExpression) {
39591+
error(leftTarget, Diagnostics.This_binary_expression_is_never_nullish_Are_you_missing_parentheses);
39592+
}
39593+
else {
39594+
if (nullishSemantics === PredicateSemantics.Always) {
39595+
error(leftTarget, Diagnostics.This_expression_is_always_nullish);
39596+
}
39597+
else {
39598+
error(leftTarget, Diagnostics.Right_operand_of_is_unreachable_because_the_left_operand_is_never_nullish);
39599+
}
39600+
}
39601+
}
39602+
}
39603+
}
39604+
39605+
function getSyntacticNullishnessSemantics(node: Node): PredicateSemantics {
39606+
node = skipOuterExpressions(node);
39607+
switch (node.kind) {
39608+
case SyntaxKind.AwaitExpression:
39609+
case SyntaxKind.CallExpression:
39610+
case SyntaxKind.ElementAccessExpression:
39611+
case SyntaxKind.NewExpression:
39612+
case SyntaxKind.PropertyAccessExpression:
39613+
case SyntaxKind.YieldExpression:
39614+
return PredicateSemantics.Sometimes;
39615+
case SyntaxKind.BinaryExpression:
39616+
// List of operators that can produce null/undefined:
39617+
// = ??= ?? || ||= && &&=
39618+
switch ((node as BinaryExpression).operatorToken.kind) {
39619+
case SyntaxKind.EqualsToken:
39620+
case SyntaxKind.QuestionQuestionToken:
39621+
case SyntaxKind.QuestionQuestionEqualsToken:
39622+
case SyntaxKind.BarBarToken:
39623+
case SyntaxKind.BarBarEqualsToken:
39624+
case SyntaxKind.AmpersandAmpersandToken:
39625+
case SyntaxKind.AmpersandAmpersandEqualsToken:
39626+
return PredicateSemantics.Sometimes;
39627+
}
39628+
return PredicateSemantics.Never;
39629+
case SyntaxKind.ConditionalExpression:
39630+
return getSyntacticNullishnessSemantics((node as ConditionalExpression).whenTrue) | getSyntacticNullishnessSemantics((node as ConditionalExpression).whenFalse);
39631+
case SyntaxKind.NullKeyword:
39632+
return PredicateSemantics.Always;
39633+
case SyntaxKind.Identifier:
39634+
if (getResolvedSymbol(node as Identifier) === undefinedSymbol) {
39635+
return PredicateSemantics.Always;
39636+
}
39637+
return PredicateSemantics.Sometimes;
3958139638
}
39639+
return PredicateSemantics.Never;
3958239640
}
3958339641

3958439642
// Note that this and `checkBinaryExpression` above should behave mostly the same, except this elides some
@@ -39589,7 +39647,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3958939647
return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === SyntaxKind.ThisKeyword);
3959039648
}
3959139649
let leftType: Type;
39592-
if (isLogicalOrCoalescingBinaryOperator(operator)) {
39650+
if (isBinaryLogicalOperator(operator)) {
3959339651
leftType = checkTruthinessExpression(left, checkMode);
3959439652
}
3959539653
else {
@@ -44217,9 +44275,57 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4421744275
if (type.flags & TypeFlags.Void) {
4421844276
error(node, Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness);
4421944277
}
44278+
else {
44279+
const semantics = getSyntacticTruthySemantics(node);
44280+
if (semantics !== PredicateSemantics.Sometimes) {
44281+
error(
44282+
node,
44283+
semantics === PredicateSemantics.Always ?
44284+
Diagnostics.This_kind_of_expression_is_always_truthy :
44285+
Diagnostics.This_kind_of_expression_is_always_falsy,
44286+
);
44287+
}
44288+
}
44289+
4422044290
return type;
4422144291
}
4422244292

44293+
function getSyntacticTruthySemantics(node: Node): PredicateSemantics {
44294+
node = skipOuterExpressions(node);
44295+
switch (node.kind) {
44296+
case SyntaxKind.NumericLiteral:
44297+
// Allow `while(0)` or `while(1)`
44298+
if ((node as NumericLiteral).text === "0" || (node as NumericLiteral).text === "1") {
44299+
return PredicateSemantics.Sometimes;
44300+
}
44301+
return PredicateSemantics.Always;
44302+
case SyntaxKind.ArrayLiteralExpression:
44303+
case SyntaxKind.ArrowFunction:
44304+
case SyntaxKind.BigIntLiteral:
44305+
case SyntaxKind.ClassExpression:
44306+
case SyntaxKind.FunctionExpression:
44307+
case SyntaxKind.JsxElement:
44308+
case SyntaxKind.JsxSelfClosingElement:
44309+
case SyntaxKind.ObjectLiteralExpression:
44310+
case SyntaxKind.RegularExpressionLiteral:
44311+
return PredicateSemantics.Always;
44312+
case SyntaxKind.VoidExpression:
44313+
case SyntaxKind.NullKeyword:
44314+
return PredicateSemantics.Never;
44315+
case SyntaxKind.NoSubstitutionTemplateLiteral:
44316+
case SyntaxKind.StringLiteral:
44317+
return !!(node as StringLiteral | NoSubstitutionTemplateLiteral).text ? PredicateSemantics.Always : PredicateSemantics.Never;
44318+
case SyntaxKind.ConditionalExpression:
44319+
return getSyntacticTruthySemantics((node as ConditionalExpression).whenTrue) | getSyntacticTruthySemantics((node as ConditionalExpression).whenFalse);
44320+
case SyntaxKind.Identifier:
44321+
if (getResolvedSymbol(node as Identifier) === undefinedSymbol) {
44322+
return PredicateSemantics.Never;
44323+
}
44324+
return PredicateSemantics.Sometimes;
44325+
}
44326+
return PredicateSemantics.Sometimes;
44327+
}
44328+
4422344329
function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) {
4422444330
return checkTruthinessOfType(checkExpression(node, checkMode), node);
4422544331
}

src/compiler/diagnosticMessages.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3915,7 +3915,26 @@
39153915
"category": "Error",
39163916
"code": 2868
39173917
},
3918-
3918+
"Right operand of ?? is unreachable because the left operand is never nullish.": {
3919+
"category": "Error",
3920+
"code": 2869
3921+
},
3922+
"This binary expression is never nullish. Are you missing parentheses?": {
3923+
"category": "Error",
3924+
"code": 2870
3925+
},
3926+
"This expression is always nullish.": {
3927+
"category": "Error",
3928+
"code": 2871
3929+
},
3930+
"This kind of expression is always truthy.": {
3931+
"category": "Error",
3932+
"code": 2872
3933+
},
3934+
"This kind of expression is always falsy.": {
3935+
"category": "Error",
3936+
"code": 2873
3937+
},
39193938
"Import declaration '{0}' is using private name '{1}'.": {
39203939
"category": "Error",
39213940
"code": 4000

src/compiler/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,14 @@ export const enum RelationComparisonResult {
923923
Overflow = ComplexityOverflow | StackDepthOverflow,
924924
}
925925

926+
/** @internal */
927+
export const enum PredicateSemantics {
928+
None = 0,
929+
Always = 1 << 0,
930+
Never = 1 << 1,
931+
Sometimes = Always | Never,
932+
}
933+
926934
/** @internal */
927935
export type NodeId = number;
928936

src/compiler/utilities.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7190,7 +7190,8 @@ export function modifierToFlag(token: SyntaxKind): ModifierFlags {
71907190
return ModifierFlags.None;
71917191
}
71927192

7193-
function isBinaryLogicalOperator(token: SyntaxKind): boolean {
7193+
/** @internal */
7194+
export function isBinaryLogicalOperator(token: SyntaxKind): boolean {
71947195
return token === SyntaxKind.BarBarToken || token === SyntaxKind.AmpersandAmpersandToken;
71957196
}
71967197

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
aliasUsageInOrExpression_main.ts(10,40): error TS2873: This kind of expression is always falsy.
2+
aliasUsageInOrExpression_main.ts(11,40): error TS2873: This kind of expression is always falsy.
3+
4+
5+
==== aliasUsageInOrExpression_main.ts (2 errors) ====
6+
import Backbone = require("./aliasUsageInOrExpression_backbone");
7+
import moduleA = require("./aliasUsageInOrExpression_moduleA");
8+
interface IHasVisualizationModel {
9+
VisualizationModel: typeof Backbone.Model;
10+
}
11+
var i: IHasVisualizationModel;
12+
var d1 = i || moduleA;
13+
var d2: IHasVisualizationModel = i || moduleA;
14+
var d2: IHasVisualizationModel = moduleA || i;
15+
var e: { x: IHasVisualizationModel } = <{ x: IHasVisualizationModel }>null || { x: moduleA };
16+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17+
!!! error TS2873: This kind of expression is always falsy.
18+
var f: { x: IHasVisualizationModel } = <{ x: IHasVisualizationModel }>null ? { x: moduleA } : null;
19+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20+
!!! error TS2873: This kind of expression is always falsy.
21+
==== aliasUsageInOrExpression_backbone.ts (0 errors) ====
22+
export class Model {
23+
public someData: string;
24+
}
25+
26+
==== aliasUsageInOrExpression_moduleA.ts (0 errors) ====
27+
import Backbone = require("./aliasUsageInOrExpression_backbone");
28+
export class VisualizationModel extends Backbone.Model {
29+
// interesting stuff here
30+
}
31+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
bestCommonTypeWithContextualTyping.ts(19,31): error TS2873: This kind of expression is always falsy.
2+
3+
4+
==== bestCommonTypeWithContextualTyping.ts (1 errors) ====
5+
interface Contextual {
6+
dummy;
7+
p?: number;
8+
}
9+
10+
interface Ellement {
11+
dummy;
12+
p: any;
13+
}
14+
15+
var e: Ellement;
16+
17+
// All of these should pass. Neither type is a supertype of the other, but the RHS should
18+
// always use Ellement in these examples (not Contextual). Because Ellement is assignable
19+
// to Contextual, no errors.
20+
var arr: Contextual[] = [e]; // Ellement[]
21+
var obj: { [s: string]: Contextual } = { s: e }; // { s: Ellement; [s: string]: Ellement }
22+
23+
var conditional: Contextual = null ? e : e; // Ellement
24+
~~~~
25+
!!! error TS2873: This kind of expression is always falsy.
26+
var contextualOr: Contextual = e || e; // Ellement

tests/baselines/reference/bestCommonTypeWithContextualTyping.types

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
interface Contextual {
55
dummy;
66
>dummy : any
7+
> : ^^^
78

89
p?: number;
910
>p : number
@@ -13,9 +14,11 @@ interface Contextual {
1314
interface Ellement {
1415
dummy;
1516
>dummy : any
17+
> : ^^^
1618

1719
p: any;
1820
>p : any
21+
> : ^^^
1922
}
2023

2124
var e: Ellement;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
returns.js(20,12): error TS2872: This kind of expression is always truthy.
2+
3+
4+
==== returns.js (1 errors) ====
5+
// @ts-check
6+
/**
7+
* @returns {string} This comment is not currently exposed
8+
*/
9+
function f() {
10+
return "hello";
11+
}
12+
13+
/**
14+
* @returns {string=} This comment is not currently exposed
15+
*/
16+
function f1() {
17+
return "hello world";
18+
}
19+
20+
/**
21+
* @returns {string|number} This comment is not currently exposed
22+
*/
23+
function f2() {
24+
return 5 || "hello";
25+
~
26+
!!! error TS2872: This kind of expression is always truthy.
27+
}
28+

tests/baselines/reference/checkJsdocReturnTag2.errors.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
returns.js(6,5): error TS2322: Type 'number' is not assignable to type 'string'.
22
returns.js(13,5): error TS2322: Type 'number | boolean' is not assignable to type 'string | number'.
33
Type 'boolean' is not assignable to type 'string | number'.
4+
returns.js(13,12): error TS2872: This kind of expression is always truthy.
45

56

6-
==== returns.js (2 errors) ====
7+
==== returns.js (3 errors) ====
78
// @ts-check
89
/**
910
* @returns {string} This comment is not currently exposed
@@ -22,5 +23,7 @@ returns.js(13,5): error TS2322: Type 'number | boolean' is not assignable to typ
2223
~~~~~~
2324
!!! error TS2322: Type 'number | boolean' is not assignable to type 'string | number'.
2425
!!! error TS2322: Type 'boolean' is not assignable to type 'string | number'.
26+
~
27+
!!! error TS2872: This kind of expression is always truthy.
2528
}
2629

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
computedPropertyNames46_ES5.ts(2,6): error TS2873: This kind of expression is always falsy.
2+
3+
4+
==== computedPropertyNames46_ES5.ts (1 errors) ====
5+
var o = {
6+
["" || 0]: 0
7+
~~
8+
!!! error TS2873: This kind of expression is always falsy.
9+
};

0 commit comments

Comments
 (0)