Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flag non-nullable functions in if statements as errors (tree walk version) #33178

Merged
merged 5 commits into from Sep 25, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 53 additions & 1 deletion src/compiler/checker.ts
Expand Up @@ -27930,7 +27930,8 @@ namespace ts {
// Grammar checking
checkGrammarStatementInAmbientContext(node);

checkTruthinessExpression(node.expression);
const type = checkTruthinessExpression(node.expression);
checkTestingKnownTruthyCallableType(node, type);
checkSourceElement(node.thenStatement);

if (node.thenStatement.kind === SyntaxKind.EmptyStatement) {
Expand All @@ -27940,6 +27941,57 @@ namespace ts {
checkSourceElement(node.elseStatement);
}

function checkTestingKnownTruthyCallableType(ifStatement: IfStatement, type: Type) {
if (!strictNullChecks) {
return;
}

const testedNode = isIdentifier(ifStatement.expression)
? ifStatement.expression
: isPropertyAccessExpression(ifStatement.expression)
? ifStatement.expression.name
: undefined;

if (!testedNode) {
return;
}

const possiblyFalsy = getFalsyFlags(type);
if (possiblyFalsy) {
return;
}

// While it technically should be invalid for any known-truthy value
// to be tested, we de-scope to functions unrefenced in the block as a
// heuristic to identify the most common bugs. There are too many
// false positives for values sourced from type definitions without
// strictNullChecks otherwise.
const callSignatures = getSignaturesOfType(type, SignatureKind.Call);
if (callSignatures.length === 0) {
return;
}

const testedFunctionSymbol = getSymbolAtLocation(testedNode);
if (!testedFunctionSymbol) {
return;
}

const functionIsUsedInBody = forEachChild(ifStatement.thenStatement, function check(childNode): boolean | undefined {
if (isIdentifier(childNode)) {
const childSymbol = getSymbolAtLocation(childNode);
if (childSymbol && childSymbol.id === testedFunctionSymbol.id) {
return true;
}
}

return forEachChild(childNode, check);
});

if (!functionIsUsedInBody) {
error(ifStatement.expression, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead);
}
}

function checkDoStatement(node: DoStatement) {
// Grammar checking
checkGrammarStatementInAmbientContext(node);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/core.ts
Expand Up @@ -1653,7 +1653,7 @@ namespace ts {
*/
export function compose<T>(...args: ((t: T) => T)[]): (t: T) => T;
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 {
if (e) {
if (!!e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty good example of what TS would now consider suspicious-enough code to error on. Making e optional would also have fixed this.

const args: ((t: T) => T)[] = [];
for (let i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/diagnosticMessages.json
Expand Up @@ -2689,7 +2689,10 @@
"category": "Error",
"code": 2773
},

"This condition will always return true since the function is always defined. Did you mean to call it instead?": {
"category": "Error",
"code": 2774
},
"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
"code": 4000
Expand Down
22 changes: 10 additions & 12 deletions src/compiler/program.ts
Expand Up @@ -1362,18 +1362,16 @@ namespace ts {
// try to verify results of module resolution
for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) {
const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.originalFileName, currentDirectory);
if (resolveModuleNamesWorker) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like resolveModuleNamesWorker is always assigned above, so this was a dead check

const moduleNames = getModuleNames(newSourceFile);
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile);
// ensure that module resolution results are still correct
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
if (resolutionsChanged) {
oldProgram.structureIsReused = StructureIsReused.SafeModules;
newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions);
}
else {
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
}
const moduleNames = getModuleNames(newSourceFile);
const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile);
// ensure that module resolution results are still correct
const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo);
if (resolutionsChanged) {
oldProgram.structureIsReused = StructureIsReused.SafeModules;
newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions);
}
else {
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
}
if (resolveTypeReferenceDirectiveNamesWorker) {
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
Expand Down
@@ -0,0 +1,91 @@
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?
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?
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?
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?
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?


==== tests/cases/compiler/truthinessCallExpressionCoercion.ts (5 errors) ====
function onlyErrorsWhenTestingNonNullableFunctionType(required: () => boolean, optional?: () => boolean) {
if (required) { // error
~~~~~~~~
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
}

if (optional) { // ok
}

if (!!required) { // ok
}

if (required()) { // ok
}
}

function onlyErrorsWhenUnusedInBody() {
function test() { return Math.random() > 0.5; }

if (test) { // error
~~~~
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
console.log('test');
}

if (test) { // ok
console.log(test);
}

if (test) { // ok
test();
}

if (test) { // ok
[() => null].forEach(() => {
test();
});
}

if (test) { // error
~~~~
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
[() => null].forEach(test => {
test();
});
}
}

function checksPropertyAccess() {
const x = {
foo: {
bar() { return true; }
}
}

if (x.foo.bar) { // error
~~~~~~~~~
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
}

if (x.foo.bar) { // ok
x.foo.bar;
}
}

class Foo {
maybeIsUser?: () => boolean;

isUser() {
return true;
}

test() {
if (this.isUser) { // error
~~~~~~~~~~~
!!! error TS2774: This condition will always return true since the function is always defined. Did you mean to call it instead?
}

if (this.maybeIsUser) { // ok
}
}
}

134 changes: 134 additions & 0 deletions tests/baselines/reference/truthinessCallExpressionCoercion.js
@@ -0,0 +1,134 @@
//// [truthinessCallExpressionCoercion.ts]
function onlyErrorsWhenTestingNonNullableFunctionType(required: () => boolean, optional?: () => boolean) {
if (required) { // error
}

if (optional) { // ok
}

if (!!required) { // ok
}

if (required()) { // ok
}
}

function onlyErrorsWhenUnusedInBody() {
function test() { return Math.random() > 0.5; }

if (test) { // error
console.log('test');
}

if (test) { // ok
console.log(test);
}

if (test) { // ok
test();
}

if (test) { // ok
[() => null].forEach(() => {
test();
});
}

if (test) { // error
[() => null].forEach(test => {
test();
});
}
}

function checksPropertyAccess() {
const x = {
foo: {
bar() { return true; }
}
}

if (x.foo.bar) { // error
}

if (x.foo.bar) { // ok
x.foo.bar;
}
}

class Foo {
maybeIsUser?: () => boolean;

isUser() {
return true;
}

test() {
if (this.isUser) { // error
}

if (this.maybeIsUser) { // ok
}
}
}


//// [truthinessCallExpressionCoercion.js]
function onlyErrorsWhenTestingNonNullableFunctionType(required, optional) {
if (required) { // error
}
if (optional) { // ok
}
if (!!required) { // ok
}
if (required()) { // ok
}
}
function onlyErrorsWhenUnusedInBody() {
function test() { return Math.random() > 0.5; }
if (test) { // error
console.log('test');
}
if (test) { // ok
console.log(test);
}
if (test) { // ok
test();
}
if (test) { // ok
[function () { return null; }].forEach(function () {
test();
});
}
if (test) { // error
[function () { return null; }].forEach(function (test) {
test();
});
}
}
function checksPropertyAccess() {
var x = {
foo: {
bar: function () { return true; }
}
};
if (x.foo.bar) { // error
}
if (x.foo.bar) { // ok
x.foo.bar;
}
}
var Foo = /** @class */ (function () {
function Foo() {
}
Foo.prototype.isUser = function () {
return true;
};
Foo.prototype.test = function () {
if (this.isUser) { // error
}
if (this.maybeIsUser) { // ok
}
};
return Foo;
}());