Skip to content

Commit 3c9e338

Browse files
committed
Only error when testing functions that are not used in the following block
1 parent 006a327 commit 3c9e338

8 files changed

+436
-190
lines changed

src/compiler/checker.ts

+52-18
Original file line numberDiff line numberDiff line change
@@ -27931,24 +27931,7 @@ namespace ts {
2793127931
checkGrammarStatementInAmbientContext(node);
2793227932

2793327933
const type = checkTruthinessExpression(node.expression);
27934-
27935-
if (strictNullChecks &&
27936-
(node.expression.kind === SyntaxKind.Identifier ||
27937-
node.expression.kind === SyntaxKind.PropertyAccessExpression ||
27938-
node.expression.kind === SyntaxKind.ElementAccessExpression)) {
27939-
const possiblyFalsy = !!getFalsyFlags(type);
27940-
if (!possiblyFalsy) {
27941-
// While it technically should be invalid for any known-truthy value
27942-
// to be tested, we de-scope to functions as a heuristic to identify
27943-
// the most common bugs. There are too many false positives for values
27944-
// sourced from type definitions without strictNullChecks otherwise.
27945-
const callSignatures = getSignaturesOfType(type, SignatureKind.Call);
27946-
if (callSignatures.length > 0) {
27947-
error(node.expression, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead);
27948-
}
27949-
}
27950-
}
27951-
27934+
checkTestingKnownTruthyCallableType(node, type);
2795227935
checkSourceElement(node.thenStatement);
2795327936

2795427937
if (node.thenStatement.kind === SyntaxKind.EmptyStatement) {
@@ -27958,6 +27941,57 @@ namespace ts {
2795827941
checkSourceElement(node.elseStatement);
2795927942
}
2796027943

27944+
function checkTestingKnownTruthyCallableType(ifStatement: IfStatement, type: Type) {
27945+
if (!strictNullChecks) {
27946+
return;
27947+
}
27948+
27949+
const testedNode = isIdentifier(ifStatement.expression)
27950+
? ifStatement.expression
27951+
: isPropertyAccessExpression(ifStatement.expression)
27952+
? ifStatement.expression.name
27953+
: undefined;
27954+
27955+
if (!testedNode) {
27956+
return;
27957+
}
27958+
27959+
const possiblyFalsy = getFalsyFlags(type);
27960+
if (possiblyFalsy) {
27961+
return;
27962+
}
27963+
27964+
// While it technically should be invalid for any known-truthy value
27965+
// to be tested, we de-scope to functions unrefenced in the block as a
27966+
// heuristic to identify the most common bugs. There are too many
27967+
// false positives for values sourced from type definitions without
27968+
// strictNullChecks otherwise.
27969+
const callSignatures = getSignaturesOfType(type, SignatureKind.Call);
27970+
if (callSignatures.length === 0) {
27971+
return;
27972+
}
27973+
27974+
const testedFunctionSymbol = getSymbolAtLocation(testedNode);
27975+
if (!testedFunctionSymbol) {
27976+
return;
27977+
}
27978+
27979+
const functionIsUsedInBody = forEachChild(ifStatement.thenStatement, function check(childNode): boolean | undefined {
27980+
if (isIdentifier(childNode)) {
27981+
const childSymbol = getSymbolAtLocation(childNode);
27982+
if (childSymbol && childSymbol.id === testedFunctionSymbol.id) {
27983+
return true;
27984+
}
27985+
}
27986+
27987+
return forEachChild(childNode, check);
27988+
});
27989+
27990+
if (!functionIsUsedInBody) {
27991+
error(ifStatement.expression, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead);
27992+
}
27993+
}
27994+
2796127995
function checkDoStatement(node: DoStatement) {
2796227996
// Grammar checking
2796327997
checkGrammarStatementInAmbientContext(node);

src/compiler/core.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1653,7 +1653,7 @@ namespace ts {
16531653
*/
16541654
export function compose<T>(...args: ((t: T) => T)[]): (t: T) => T;
16551655
export function compose<T>(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T {
1656-
if (e) {
1656+
if (!!e) {
16571657
const args: ((t: T) => T)[] = [];
16581658
for (let i = 0; i < arguments.length; i++) {
16591659
args[i] = arguments[i];

src/compiler/program.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -1362,18 +1362,16 @@ namespace ts {
13621362
// try to verify results of module resolution
13631363
for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) {
13641364
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.originalFileName, currentDirectory);
1365-
if (resolveModuleNamesWorker) {
1366-
const moduleNames = getModuleNames(newSourceFile);
1367-
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile);
1368-
// ensure that module resolution results are still correct
1369-
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
1370-
if (resolutionsChanged) {
1371-
oldProgram.structureIsReused = StructureIsReused.SafeModules;
1372-
newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions);
1373-
}
1374-
else {
1375-
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
1376-
}
1365+
const moduleNames = getModuleNames(newSourceFile);
1366+
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile);
1367+
// ensure that module resolution results are still correct
1368+
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
1369+
if (resolutionsChanged) {
1370+
oldProgram.structureIsReused = StructureIsReused.SafeModules;
1371+
newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions);
1372+
}
1373+
else {
1374+
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
13771375
}
13781376
if (resolveTypeReferenceDirectiveNamesWorker) {
13791377
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.

tests/baselines/reference/truthinessCallExpressionCoercion.errors.txt

+46-24
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,73 @@
1-
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?
2-
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?
4-
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?
1+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(2,9): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
2+
tests/cases/compiler/truthinessCallExpressionCoercion.ts(18,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(36,9): 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(50,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(66,13): error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
66

77

88
==== tests/cases/compiler/truthinessCallExpressionCoercion.ts (5 errors) ====
9-
function func() { return Math.random() > 0.5; }
10-
11-
if (func) { // error
12-
~~~~
13-
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
14-
}
15-
16-
function onlyErrorsWhenNonNullable(required: () => boolean, optional?: () => boolean) {
9+
function onlyErrorsWhenTestingNonNullableFunctionType(required: () => boolean, optional?: () => boolean) {
1710
if (required) { // error
1811
~~~~~~~~
1912
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
2013
}
2114

15+
if (optional) { // ok
16+
}
17+
18+
if (!!required) { // ok
19+
}
20+
2221
if (required()) { // ok
2322
}
23+
}
2424

25-
if (optional) { // ok
25+
function onlyErrorsWhenUnusedInBody() {
26+
function test() { return Math.random() > 0.5; }
27+
28+
if (test) { // error
29+
~~~~
30+
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
31+
console.log('test');
32+
}
33+
34+
if (test) { // ok
35+
console.log(test);
36+
}
37+
38+
if (test) { // ok
39+
test();
40+
}
41+
42+
if (test) { // ok
43+
[() => null].forEach(() => {
44+
test();
45+
});
46+
}
47+
48+
if (test) { // error
49+
~~~~
50+
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
51+
[() => null].forEach(test => {
52+
test();
53+
});
2654
}
2755
}
2856

29-
function checksPropertyAndElementAccess() {
57+
function checksPropertyAccess() {
3058
const x = {
3159
foo: {
32-
bar() { }
60+
bar() { return true; }
3361
}
3462
}
3563

3664
if (x.foo.bar) { // error
3765
~~~~~~~~~
3866
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
3967
}
40-
41-
if (x.foo['bar']) { // 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-
}
4668

47-
function maybeBoolean(param: boolean | (() => boolean)) {
48-
if (param) { // ok
69+
if (x.foo.bar) { // ok
70+
x.foo.bar;
4971
}
5072
}
5173

tests/baselines/reference/truthinessCallExpressionCoercion.js

+66-26
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,58 @@
11
//// [truthinessCallExpressionCoercion.ts]
2-
function func() { return Math.random() > 0.5; }
2+
function onlyErrorsWhenTestingNonNullableFunctionType(required: () => boolean, optional?: () => boolean) {
3+
if (required) { // error
4+
}
35

4-
if (func) { // error
5-
}
6+
if (optional) { // ok
7+
}
68

7-
function onlyErrorsWhenNonNullable(required: () => boolean, optional?: () => boolean) {
8-
if (required) { // error
9+
if (!!required) { // ok
910
}
1011

1112
if (required()) { // ok
1213
}
14+
}
1315

14-
if (optional) { // ok
16+
function onlyErrorsWhenUnusedInBody() {
17+
function test() { return Math.random() > 0.5; }
18+
19+
if (test) { // error
20+
console.log('test');
21+
}
22+
23+
if (test) { // ok
24+
console.log(test);
25+
}
26+
27+
if (test) { // ok
28+
test();
29+
}
30+
31+
if (test) { // ok
32+
[() => null].forEach(() => {
33+
test();
34+
});
35+
}
36+
37+
if (test) { // error
38+
[() => null].forEach(test => {
39+
test();
40+
});
1541
}
1642
}
1743

18-
function checksPropertyAndElementAccess() {
44+
function checksPropertyAccess() {
1945
const x = {
2046
foo: {
21-
bar() { }
47+
bar() { return true; }
2248
}
2349
}
2450

2551
if (x.foo.bar) { // error
2652
}
27-
28-
if (x.foo['bar']) { // error
29-
}
30-
}
3153

32-
function maybeBoolean(param: boolean | (() => boolean)) {
33-
if (param) { // ok
54+
if (x.foo.bar) { // ok
55+
x.foo.bar;
3456
}
3557
}
3658

@@ -52,30 +74,48 @@ class Foo {
5274

5375

5476
//// [truthinessCallExpressionCoercion.js]
55-
function func() { return Math.random() > 0.5; }
56-
if (func) { // error
57-
}
58-
function onlyErrorsWhenNonNullable(required, optional) {
77+
function onlyErrorsWhenTestingNonNullableFunctionType(required, optional) {
5978
if (required) { // error
6079
}
80+
if (optional) { // ok
81+
}
82+
if (!!required) { // ok
83+
}
6184
if (required()) { // ok
6285
}
63-
if (optional) { // ok
86+
}
87+
function onlyErrorsWhenUnusedInBody() {
88+
function test() { return Math.random() > 0.5; }
89+
if (test) { // error
90+
console.log('test');
91+
}
92+
if (test) { // ok
93+
console.log(test);
94+
}
95+
if (test) { // ok
96+
test();
97+
}
98+
if (test) { // ok
99+
[function () { return null; }].forEach(function () {
100+
test();
101+
});
102+
}
103+
if (test) { // error
104+
[function () { return null; }].forEach(function (test) {
105+
test();
106+
});
64107
}
65108
}
66-
function checksPropertyAndElementAccess() {
109+
function checksPropertyAccess() {
67110
var x = {
68111
foo: {
69-
bar: function () { }
112+
bar: function () { return true; }
70113
}
71114
};
72115
if (x.foo.bar) { // error
73116
}
74-
if (x.foo['bar']) { // error
75-
}
76-
}
77-
function maybeBoolean(param) {
78-
if (param) { // ok
117+
if (x.foo.bar) { // ok
118+
x.foo.bar;
79119
}
80120
}
81121
var Foo = /** @class */ (function () {

0 commit comments

Comments
 (0)