Skip to content

Commit 3906a1e

Browse files
committed
Only error when testing functions that return booleans
1 parent 130615a commit 3906a1e

6 files changed

+288
-70
lines changed

src/compiler/checker.ts

+47-18
Original file line numberDiff line numberDiff line change
@@ -27875,24 +27875,7 @@ namespace ts {
2787527875
checkGrammarStatementInAmbientContext(node);
2787627876

2787727877
const type = checkTruthinessExpression(node.expression);
27878-
27879-
if (strictNullChecks &&
27880-
(node.expression.kind === SyntaxKind.Identifier ||
27881-
node.expression.kind === SyntaxKind.PropertyAccessExpression ||
27882-
node.expression.kind === SyntaxKind.ElementAccessExpression)) {
27883-
const possiblyFalsy = !!getFalsyFlags(type);
27884-
if (!possiblyFalsy) {
27885-
// While it technically should be invalid for any known-truthy value
27886-
// to be tested, we de-scope to functions as a heuristic to identify
27887-
// the most common bugs. There are too many false positives for values
27888-
// sourced from type definitions without strictNullChecks otherwise.
27889-
const callSignatures = getSignaturesOfType(type, SignatureKind.Call);
27890-
if (callSignatures.length > 0) {
27891-
error(node.expression, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead);
27892-
}
27893-
}
27894-
}
27895-
27878+
checkTestingKnownTruthyCallableType(node.expression, type);
2789627879
checkSourceElement(node.thenStatement);
2789727880

2789827881
if (node.thenStatement.kind === SyntaxKind.EmptyStatement) {
@@ -27902,6 +27885,52 @@ namespace ts {
2790227885
checkSourceElement(node.elseStatement);
2790327886
}
2790427887

27888+
function checkTestingKnownTruthyCallableType(testedNode: Expression, type: Type) {
27889+
if (!strictNullChecks) {
27890+
return;
27891+
}
27892+
27893+
if (testedNode.kind === SyntaxKind.Identifier ||
27894+
testedNode.kind === SyntaxKind.PropertyAccessExpression ||
27895+
testedNode.kind === SyntaxKind.ElementAccessExpression) {
27896+
const possiblyFalsy = getFalsyFlags(type);
27897+
if (possiblyFalsy) {
27898+
return;
27899+
}
27900+
27901+
// While it technically should be invalid for any known-truthy value
27902+
// to be tested, we de-scope to functions that return a boolean as a
27903+
// heuristic to identify the most common bugs. There are too many
27904+
// false positives for values sourced from type definitions without
27905+
// strictNullChecks otherwise.
27906+
const callSignatures = getSignaturesOfType(type, SignatureKind.Call);
27907+
if (callSignatures.length === 0) {
27908+
return;
27909+
}
27910+
27911+
const alwaysReturnsBoolean = every(callSignatures, signature => {
27912+
const returnType = getReturnTypeOfSignature(signature);
27913+
const exactlyBoolean = TypeFlags.Boolean | TypeFlags.Union;
27914+
if ((returnType.flags & exactlyBoolean) === exactlyBoolean) {
27915+
return true;
27916+
}
27917+
27918+
if (returnType.flags & TypeFlags.Union) {
27919+
const allowedInUnion = TypeFlags.Boolean | TypeFlags.BooleanLiteral | TypeFlags.Nullable;
27920+
return every((returnType as UnionType).types, innerType => {
27921+
return (innerType.flags | allowedInUnion) === allowedInUnion;
27922+
});
27923+
}
27924+
27925+
return false;
27926+
});
27927+
27928+
if (alwaysReturnsBoolean) {
27929+
error(testedNode, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead);
27930+
}
27931+
}
27932+
}
27933+
2790527934
function checkDoStatement(node: DoStatement) {
2790627935
// Grammar checking
2790727936
checkGrammarStatementInAmbientContext(node);

tests/baselines/reference/truthinessCallExpressionCoercion.errors.txt

+36-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
tests/cases/compiler/truthinessCallExpressionCoercion.ts(3,5): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
22
tests/cases/compiler/truthinessCallExpressionCoercion.ts(7,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
3-
tests/cases/compiler/truthinessCallExpressionCoercion.ts(24,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
43
tests/cases/compiler/truthinessCallExpressionCoercion.ts(27,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
5-
tests/cases/compiler/truthinessCallExpressionCoercion.ts(44,13): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
4+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(30,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
5+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(50,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
6+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(53,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
7+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(70,13): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
68

79

8-
==== tests/cases/compiler/truthinessCallExpressionCoercion.ts (5 errors) ====
10+
==== tests/cases/compiler/truthinessCallExpressionCoercion.ts (7 errors) ====
911
function func() { return Math.random() > 0.5; }
1012

1113
if (func) { // error
@@ -19,17 +21,47 @@ tests/cases/compiler/truthinessCallExpressionCoercion.ts(44,13): error TS2774: T
1921
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
2022
}
2123

24+
if (!!required) { // ok
25+
}
26+
2227
if (required()) { // ok
2328
}
2429

2530
if (optional) { // ok
2631
}
2732
}
2833

34+
function onlyErrorsWhenReturnsBoolean(
35+
bool: () => boolean,
36+
nullableBool: () => boolean | undefined,
37+
notABool: () => string,
38+
unionWithBool: () => boolean | string,
39+
nullableString: () => string | undefined
40+
) {
41+
if (bool) { // error
42+
~~~~
43+
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
44+
}
45+
46+
if (nullableBool) { // error
47+
~~~~~~~~~~~~
48+
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
49+
}
50+
51+
if (notABool) { // ok
52+
}
53+
54+
if (unionWithBool) { // ok
55+
}
56+
57+
if (nullableString) { // ok
58+
}
59+
}
60+
2961
function checksPropertyAndElementAccess() {
3062
const x = {
3163
foo: {
32-
bar() { }
64+
bar() { return true; }
3365
}
3466
}
3567

tests/baselines/reference/truthinessCallExpressionCoercion.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,43 @@ function onlyErrorsWhenNonNullable(required: () => boolean, optional?: () => boo
88
if (required) { // error
99
}
1010

11+
if (!!required) { // ok
12+
}
13+
1114
if (required()) { // ok
1215
}
1316

1417
if (optional) { // ok
1518
}
1619
}
1720

21+
function onlyErrorsWhenReturnsBoolean(
22+
bool: () => boolean,
23+
nullableBool: () => boolean | undefined,
24+
notABool: () => string,
25+
unionWithBool: () => boolean | string,
26+
nullableString: () => string | undefined
27+
) {
28+
if (bool) { // error
29+
}
30+
31+
if (nullableBool) { // error
32+
}
33+
34+
if (notABool) { // ok
35+
}
36+
37+
if (unionWithBool) { // ok
38+
}
39+
40+
if (nullableString) { // ok
41+
}
42+
}
43+
1844
function checksPropertyAndElementAccess() {
1945
const x = {
2046
foo: {
21-
bar() { }
47+
bar() { return true; }
2248
}
2349
}
2450

@@ -58,15 +84,29 @@ if (func) { // error
5884
function onlyErrorsWhenNonNullable(required, optional) {
5985
if (required) { // error
6086
}
87+
if (!!required) { // ok
88+
}
6189
if (required()) { // ok
6290
}
6391
if (optional) { // ok
6492
}
6593
}
94+
function onlyErrorsWhenReturnsBoolean(bool, nullableBool, notABool, unionWithBool, nullableString) {
95+
if (bool) { // error
96+
}
97+
if (nullableBool) { // error
98+
}
99+
if (notABool) { // ok
100+
}
101+
if (unionWithBool) { // ok
102+
}
103+
if (nullableString) { // ok
104+
}
105+
}
66106
function checksPropertyAndElementAccess() {
67107
var x = {
68108
foo: {
69-
bar: function () { }
109+
bar: function () { return true; }
70110
}
71111
};
72112
if (x.foo.bar) { // error

tests/baselines/reference/truthinessCallExpressionCoercion.symbols

+71-27
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ function onlyErrorsWhenNonNullable(required: () => boolean, optional?: () => boo
1818
>required : Symbol(required, Decl(truthinessCallExpressionCoercion.ts, 5, 35))
1919
}
2020

21+
if (!!required) { // ok
22+
>required : Symbol(required, Decl(truthinessCallExpressionCoercion.ts, 5, 35))
23+
}
24+
2125
if (required()) { // ok
2226
>required : Symbol(required, Decl(truthinessCallExpressionCoercion.ts, 5, 35))
2327
}
@@ -27,70 +31,110 @@ function onlyErrorsWhenNonNullable(required: () => boolean, optional?: () => boo
2731
}
2832
}
2933

34+
function onlyErrorsWhenReturnsBoolean(
35+
>onlyErrorsWhenReturnsBoolean : Symbol(onlyErrorsWhenReturnsBoolean, Decl(truthinessCallExpressionCoercion.ts, 17, 1))
36+
37+
bool: () => boolean,
38+
>bool : Symbol(bool, Decl(truthinessCallExpressionCoercion.ts, 19, 38))
39+
40+
nullableBool: () => boolean | undefined,
41+
>nullableBool : Symbol(nullableBool, Decl(truthinessCallExpressionCoercion.ts, 20, 24))
42+
43+
notABool: () => string,
44+
>notABool : Symbol(notABool, Decl(truthinessCallExpressionCoercion.ts, 21, 44))
45+
46+
unionWithBool: () => boolean | string,
47+
>unionWithBool : Symbol(unionWithBool, Decl(truthinessCallExpressionCoercion.ts, 22, 27))
48+
49+
nullableString: () => string | undefined
50+
>nullableString : Symbol(nullableString, Decl(truthinessCallExpressionCoercion.ts, 23, 42))
51+
52+
) {
53+
if (bool) { // error
54+
>bool : Symbol(bool, Decl(truthinessCallExpressionCoercion.ts, 19, 38))
55+
}
56+
57+
if (nullableBool) { // error
58+
>nullableBool : Symbol(nullableBool, Decl(truthinessCallExpressionCoercion.ts, 20, 24))
59+
}
60+
61+
if (notABool) { // ok
62+
>notABool : Symbol(notABool, Decl(truthinessCallExpressionCoercion.ts, 21, 44))
63+
}
64+
65+
if (unionWithBool) { // ok
66+
>unionWithBool : Symbol(unionWithBool, Decl(truthinessCallExpressionCoercion.ts, 22, 27))
67+
}
68+
69+
if (nullableString) { // ok
70+
>nullableString : Symbol(nullableString, Decl(truthinessCallExpressionCoercion.ts, 23, 42))
71+
}
72+
}
73+
3074
function checksPropertyAndElementAccess() {
31-
>checksPropertyAndElementAccess : Symbol(checksPropertyAndElementAccess, Decl(truthinessCallExpressionCoercion.ts, 14, 1))
75+
>checksPropertyAndElementAccess : Symbol(checksPropertyAndElementAccess, Decl(truthinessCallExpressionCoercion.ts, 40, 1))
3276

3377
const x = {
34-
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 17, 9))
78+
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 43, 9))
3579

3680
foo: {
37-
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 17, 15))
81+
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 43, 15))
3882

39-
bar() { }
40-
>bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 18, 14))
83+
bar() { return true; }
84+
>bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 44, 14))
4185
}
4286
}
4387

4488
if (x.foo.bar) { // error
45-
>x.foo.bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 18, 14))
46-
>x.foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 17, 15))
47-
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 17, 9))
48-
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 17, 15))
49-
>bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 18, 14))
89+
>x.foo.bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 44, 14))
90+
>x.foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 43, 15))
91+
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 43, 9))
92+
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 43, 15))
93+
>bar : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 44, 14))
5094
}
5195

5296
if (x.foo['bar']) { // error
53-
>x.foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 17, 15))
54-
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 17, 9))
55-
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 17, 15))
56-
>'bar' : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 18, 14))
97+
>x.foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 43, 15))
98+
>x : Symbol(x, Decl(truthinessCallExpressionCoercion.ts, 43, 9))
99+
>foo : Symbol(foo, Decl(truthinessCallExpressionCoercion.ts, 43, 15))
100+
>'bar' : Symbol(bar, Decl(truthinessCallExpressionCoercion.ts, 44, 14))
57101
}
58102
}
59103

60104
function maybeBoolean(param: boolean | (() => boolean)) {
61-
>maybeBoolean : Symbol(maybeBoolean, Decl(truthinessCallExpressionCoercion.ts, 28, 1))
62-
>param : Symbol(param, Decl(truthinessCallExpressionCoercion.ts, 30, 22))
105+
>maybeBoolean : Symbol(maybeBoolean, Decl(truthinessCallExpressionCoercion.ts, 54, 1))
106+
>param : Symbol(param, Decl(truthinessCallExpressionCoercion.ts, 56, 22))
63107

64108
if (param) { // ok
65-
>param : Symbol(param, Decl(truthinessCallExpressionCoercion.ts, 30, 22))
109+
>param : Symbol(param, Decl(truthinessCallExpressionCoercion.ts, 56, 22))
66110
}
67111
}
68112

69113
class Foo {
70-
>Foo : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 33, 1))
114+
>Foo : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 59, 1))
71115

72116
maybeIsUser?: () => boolean;
73-
>maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 35, 11))
117+
>maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 61, 11))
74118

75119
isUser() {
76-
>isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 36, 32))
120+
>isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 62, 32))
77121

78122
return true;
79123
}
80124

81125
test() {
82-
>test : Symbol(Foo.test, Decl(truthinessCallExpressionCoercion.ts, 40, 5))
126+
>test : Symbol(Foo.test, Decl(truthinessCallExpressionCoercion.ts, 66, 5))
83127

84128
if (this.isUser) { // error
85-
>this.isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 36, 32))
86-
>this : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 33, 1))
87-
>isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 36, 32))
129+
>this.isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 62, 32))
130+
>this : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 59, 1))
131+
>isUser : Symbol(Foo.isUser, Decl(truthinessCallExpressionCoercion.ts, 62, 32))
88132
}
89133

90134
if (this.maybeIsUser) { // ok
91-
>this.maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 35, 11))
92-
>this : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 33, 1))
93-
>maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 35, 11))
135+
>this.maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 61, 11))
136+
>this : Symbol(Foo, Decl(truthinessCallExpressionCoercion.ts, 59, 1))
137+
>maybeIsUser : Symbol(Foo.maybeIsUser, Decl(truthinessCallExpressionCoercion.ts, 61, 11))
94138
}
95139
}
96140
}

0 commit comments

Comments
 (0)