-
Notifications
You must be signed in to change notification settings - Fork 12.8k
[WIP] Support condition always true in prefix unary expression #60068
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
base: main
Are you sure you want to change the base?
Changes from all commits
7942777
41eb788
72c3c8b
5eb8478
1274b62
9630147
16ae8dc
8f8b45c
70b70fb
fcb0c21
7a14b9d
4e47c53
5099e88
75f941d
de99724
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44536,31 +44536,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
checkSourceElement(node.elseStatement); | ||
} | ||
|
||
function checkTestingKnownTruthyCallableOrAwaitableOrEnumMemberType(condExpr: Expression, condType: Type, body?: Statement | Expression) { | ||
function checkTestingKnownTruthyCallableOrAwaitableOrEnumMemberType(inputCondExpr: Expression, condType: Type, body?: Statement | Expression) { | ||
if (!strictNullChecks) return; | ||
bothHelper(condExpr, body); | ||
|
||
function bothHelper(condExpr: Expression, body: Expression | Statement | undefined) { | ||
condExpr = skipParentheses(condExpr); | ||
|
||
helper(condExpr, body); | ||
bothHelper(inputCondExpr); | ||
|
||
function bothHelper(condExpr: Expression) { | ||
condExpr = skipParentheses(condExpr); | ||
helper(condExpr); | ||
while (isBinaryExpression(condExpr) && (condExpr.operatorToken.kind === SyntaxKind.BarBarToken || condExpr.operatorToken.kind === SyntaxKind.QuestionQuestionToken)) { | ||
condExpr = skipParentheses(condExpr.left); | ||
helper(condExpr, body); | ||
helper(condExpr); | ||
} | ||
} | ||
|
||
function helper(condExpr: Expression, body: Expression | Statement | undefined) { | ||
const location = isLogicalOrCoalescingBinaryExpression(condExpr) ? skipParentheses(condExpr.right) : condExpr; | ||
function helper(condExpr: Expression) { | ||
let inverted = false; | ||
let location = condExpr; | ||
if (isPrefixUnaryExpression(condExpr) && condExpr.operator === SyntaxKind.ExclamationToken) { | ||
location = skipParentheses(condExpr.operand); | ||
inverted = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thing to consider (and probably add a test) is that we can have an expression that flips between the negative and positive case, if we have nested negation expressions, e.g.: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah 🙈 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's really any special handling needed here? |
||
} | ||
location = isLogicalOrCoalescingBinaryExpression(location) ? skipParentheses(location.right) : location; | ||
if (isModuleExportsAccessExpression(location)) { | ||
return; | ||
} | ||
if (isLogicalOrCoalescingBinaryExpression(location)) { | ||
bothHelper(location, body); | ||
bothHelper(location); | ||
return; | ||
} | ||
const type = location === condExpr ? condType : checkExpression(location); | ||
const type = location === inputCondExpr ? condType : checkExpression(location); | ||
if (type.flags & TypeFlags.EnumLiteral && isPropertyAccessExpression(location) && (getNodeLinks(location.expression).resolvedSymbol ?? unknownSymbol).flags & SymbolFlags.Enum) { | ||
// EnumLiteral type at condition with known value is always truthy or always falsy, likely an error | ||
error(location, Diagnostics.This_condition_will_always_return_0, !!(type as LiteralType).value ? "true" : "false"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this was a bug before? It's an optimisation to not need to call |
||
|
@@ -44575,8 +44580,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
// are too many false positives for values sourced from type | ||
// definitions without strictNullChecks otherwise. | ||
const callSignatures = getSignaturesOfType(type, SignatureKind.Call); | ||
const isFnCall = callSignatures.length > 0; | ||
const isPromise = !!getAwaitedTypeOfPromise(type); | ||
if (callSignatures.length === 0 && !isPromise) { | ||
if (!isFnCall && !isPromise) { | ||
return; | ||
} | ||
|
||
|
@@ -44588,8 +44594,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
return; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For some reason without this it wouldn't detect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it probably should be For the // If the test was a simple identifier, the above check is sufficient check. Otherwise it's treating it as a nested object, which will fail |
||
const isUsed = testedSymbol && isBinaryExpression(condExpr.parent) && isSymbolUsedInBinaryExpressionChain(condExpr.parent, testedSymbol) | ||
|| testedSymbol && body && isSymbolUsedInConditionBody(condExpr, body, testedNode, testedSymbol); | ||
let isUsed: boolean; | ||
if (inverted) { | ||
const closestBlock = findAncestor(condExpr.parent, isBlock); | ||
const isUsedLater = !!(testedSymbol && closestBlock && isSymbolUsedInBody(location, closestBlock, testedNode, testedSymbol, condExpr.end + 1)); | ||
isUsed = isUsedLater; | ||
} | ||
else { | ||
const isUsedInBody = !!(testedSymbol && body && isSymbolUsedInBody(location, body, testedNode, testedSymbol, 0)); | ||
isUsed = isUsedInBody || !!(testedSymbol && isBinaryExpression(condExpr.parent) && isSymbolUsedInBinaryExpressionChain(condExpr.parent, testedSymbol)); | ||
} | ||
|
||
if (!isUsed) { | ||
if (isPromise) { | ||
errorAndMaybeSuggestAwait( | ||
|
@@ -44606,15 +44621,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
} | ||
} | ||
|
||
function isSymbolUsedInConditionBody(expr: Expression, body: Statement | Expression, testedNode: Node, testedSymbol: Symbol): boolean { | ||
function isSymbolUsedInBody(expr: Expression, body: Statement | Expression | Node, testedNode: Node, testedSymbol: Symbol, startPos: number): boolean { | ||
return !!forEachChild(body, function check(childNode): boolean | undefined { | ||
if (isIdentifier(childNode)) { | ||
if (childNode.pos >= startPos && isIdentifier(childNode)) { | ||
const childSymbol = getSymbolAtLocation(childNode); | ||
if (childSymbol && childSymbol === testedSymbol) { | ||
// If the test was a simple identifier, the above check is sufficient | ||
if (isIdentifier(expr) || isIdentifier(testedNode) && isBinaryExpression(testedNode.parent)) { | ||
return true; | ||
} | ||
|
||
// Otherwise we need to ensure the symbol is called on the same target | ||
let testedExpression = testedNode.parent; | ||
let childExpression = childNode.parent; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
falsinessCallExpressionCoercion.ts(4,10): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
falsinessCallExpressionCoercion.ts(25,10): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
falsinessCallExpressionCoercion.ts(41,10): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
falsinessCallExpressionCoercion.ts(66,14): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
falsinessCallExpressionCoercion.ts(91,10): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
falsinessCallExpressionCoercion.ts(107,10): error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
|
||
|
||
==== falsinessCallExpressionCoercion.ts (6 errors) ==== | ||
function test1() { | ||
function canAccess() { return false; } | ||
|
||
if (!canAccess) { // error | ||
~~~~~~~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
} | ||
|
||
function test2() { | ||
function canAccess() { return false; } | ||
|
||
if (!canAccess) { // ok | ||
} | ||
|
||
canAccess(); | ||
} | ||
|
||
function test3() { | ||
function canAccess() { return false; } | ||
|
||
if (!!!canAccess) { // ok | ||
} | ||
} | ||
|
||
function test4(canAccess: () => boolean) { | ||
if (!canAccess) { // error | ||
~~~~~~~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
} | ||
|
||
function test5(canAccess?: () => boolean) { | ||
if (!canAccess) { // ok | ||
} | ||
} | ||
|
||
function test6() { | ||
const x = { | ||
foo: { | ||
bar() { return true; } | ||
} | ||
}; | ||
|
||
if (!x.foo.bar) { // error | ||
~~~~~~~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
} | ||
|
||
function test7() { | ||
const x = { | ||
foo: { | ||
bar() { return true; } | ||
} | ||
}; | ||
|
||
if (!x.foo.bar) { // ok | ||
} | ||
|
||
x.foo.bar(); | ||
} | ||
|
||
class Test8 { | ||
maybeIsUser?: () => boolean; | ||
|
||
isUser() { | ||
return true; | ||
} | ||
|
||
test() { | ||
if (!this.isUser) { // error | ||
~~~~~~~~~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
|
||
if (!this.maybeIsUser) { // ok | ||
} | ||
} | ||
} | ||
|
||
class Test9 { | ||
isUser() { | ||
return true; | ||
} | ||
|
||
test() { | ||
if (!this.isUser) { // ok | ||
} | ||
|
||
this.isUser(); | ||
} | ||
} | ||
|
||
function test10() { | ||
function canAccess() { return false; } | ||
|
||
const res = canAccess | ||
if (!res) { // error | ||
~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
} | ||
|
||
function test11() { | ||
function canAccess() { return false; } | ||
|
||
if (!canAccess) { // ok | ||
} else { | ||
canAccess() | ||
tjenkinson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
function test12() { | ||
function canAccess() { return false; } | ||
|
||
if (!canAccess || Math.random()) { // error | ||
~~~~~~~~~ | ||
!!! error TS2774: This condition will always return true since this function is always defined. Did you mean to call it instead? | ||
} | ||
} | ||
|
||
function test13() { | ||
function canAccess() { return false; } | ||
|
||
if (!canAccess || Math.random()) { // ok | ||
} | ||
|
||
canAccess() | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.