mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
Enhanced truthy/falsy type narrowing pattern to handle classes that contain a __bool__
method that always returns True or False.
This commit is contained in:
parent
3ff69a1ffc
commit
cae15781a3
@ -200,8 +200,6 @@ import {
|
||||
areTypesSame,
|
||||
buildTypeVarMapFromSpecializedClass,
|
||||
CanAssignFlags,
|
||||
canBeFalsy,
|
||||
canBeTruthy,
|
||||
ClassMember,
|
||||
ClassMemberLookupFlags,
|
||||
combineSameSizedTuples,
|
||||
@ -240,10 +238,8 @@ import {
|
||||
ParameterSource,
|
||||
partiallySpecializeType,
|
||||
populateTypeVarMapForSelfType,
|
||||
removeFalsinessFromType,
|
||||
removeNoReturnFromUnion,
|
||||
removeParamSpecVariadicsFromSignature,
|
||||
removeTruthinessFromType,
|
||||
requiresSpecialization,
|
||||
requiresTypeArguments,
|
||||
setTypeArgumentsRecursive,
|
||||
@ -1243,6 +1239,198 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
||||
return returnType;
|
||||
}
|
||||
|
||||
function canBeFalsy(type: Type, recursionLevel = 0): boolean {
|
||||
if (recursionLevel > maxTypeRecursionCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type.category) {
|
||||
case TypeCategory.Unbound:
|
||||
case TypeCategory.Unknown:
|
||||
case TypeCategory.Any:
|
||||
case TypeCategory.Never:
|
||||
case TypeCategory.None: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case TypeCategory.Union: {
|
||||
return findSubtype(type, (subtype) => canBeFalsy(subtype, recursionLevel + 1)) !== undefined;
|
||||
}
|
||||
|
||||
case TypeCategory.Function:
|
||||
case TypeCategory.OverloadedFunction:
|
||||
case TypeCategory.Module:
|
||||
case TypeCategory.TypeVar: {
|
||||
return false;
|
||||
}
|
||||
|
||||
case TypeCategory.Class: {
|
||||
if (TypeBase.isInstantiable(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle tuples specially.
|
||||
if (isTupleClass(type) && type.tupleTypeArguments) {
|
||||
return isOpenEndedTupleClass(type) || type.tupleTypeArguments.length === 0;
|
||||
}
|
||||
|
||||
// Check for Literal[False] and Literal[True].
|
||||
if (ClassType.isBuiltIn(type, 'bool') && type.literalValue !== undefined) {
|
||||
return type.literalValue === false;
|
||||
}
|
||||
|
||||
const lenMethod = lookUpObjectMember(type, '__len__');
|
||||
if (lenMethod) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const boolMethod = lookUpObjectMember(type, '__bool__');
|
||||
if (boolMethod) {
|
||||
const boolMethodType = getTypeOfMember(boolMethod);
|
||||
|
||||
// If the __bool__ function unconditionally returns True, it can never be falsy.
|
||||
if (isFunction(boolMethodType) && boolMethodType.details.declaredReturnType) {
|
||||
const returnType = boolMethodType.details.declaredReturnType;
|
||||
if (
|
||||
isClassInstance(returnType) &&
|
||||
ClassType.isBuiltIn(returnType, 'bool') &&
|
||||
returnType.literalValue === true
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canBeTruthy(type: Type, recursionLevel = 0): boolean {
|
||||
if (recursionLevel > maxTypeRecursionCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type.category) {
|
||||
case TypeCategory.Unknown:
|
||||
case TypeCategory.Function:
|
||||
case TypeCategory.OverloadedFunction:
|
||||
case TypeCategory.Module:
|
||||
case TypeCategory.TypeVar:
|
||||
case TypeCategory.Never:
|
||||
case TypeCategory.Any: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case TypeCategory.Union: {
|
||||
return findSubtype(type, (subtype) => canBeTruthy(subtype, recursionLevel + 1)) !== undefined;
|
||||
}
|
||||
|
||||
case TypeCategory.Unbound:
|
||||
case TypeCategory.None: {
|
||||
return false;
|
||||
}
|
||||
|
||||
case TypeCategory.Class: {
|
||||
if (TypeBase.isInstantiable(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Tuple[()] (an empty tuple).
|
||||
if (isTupleClass(type)) {
|
||||
if (type.tupleTypeArguments && type.tupleTypeArguments!.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Literal[False], Literal[0], Literal[""].
|
||||
if (type.literalValue === false || type.literalValue === 0 || type.literalValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const boolMethod = lookUpObjectMember(type, '__bool__');
|
||||
if (boolMethod) {
|
||||
const boolMethodType = getTypeOfMember(boolMethod);
|
||||
|
||||
// If the __bool__ function unconditionally returns False, it can never be truthy.
|
||||
if (isFunction(boolMethodType) && boolMethodType.details.declaredReturnType) {
|
||||
const returnType = boolMethodType.details.declaredReturnType;
|
||||
if (
|
||||
isClassInstance(returnType) &&
|
||||
ClassType.isBuiltIn(returnType, 'bool') &&
|
||||
returnType.literalValue === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filters a type such that that no part of it is definitely
|
||||
// truthy. For example, if a type is a union of None
|
||||
// and a custom class "Foo" that has no __len__ or __nonzero__
|
||||
// method, this method would strip off the "Foo"
|
||||
// and return only the "None".
|
||||
function removeTruthinessFromType(type: Type): Type {
|
||||
return mapSubtypes(type, (subtype) => {
|
||||
if (isClassInstance(subtype)) {
|
||||
if (subtype.literalValue !== undefined) {
|
||||
// If the object is already definitely falsy, it's fine to
|
||||
// include, otherwise it should be removed.
|
||||
return !subtype.literalValue ? subtype : undefined;
|
||||
}
|
||||
|
||||
// If the object is a bool, make it "false", since
|
||||
// "true" is a truthy value.
|
||||
if (ClassType.isBuiltIn(subtype, 'bool')) {
|
||||
return ClassType.cloneWithLiteral(subtype, /* value */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's possible for the type to be falsy, include it.
|
||||
if (canBeFalsy(subtype)) {
|
||||
return subtype;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Filters a type such that that no part of it is definitely
|
||||
// falsy. For example, if a type is a union of None
|
||||
// and an "int", this method would strip off the "None"
|
||||
// and return only the "int".
|
||||
function removeFalsinessFromType(type: Type): Type {
|
||||
return mapSubtypes(type, (subtype) => {
|
||||
if (isClassInstance(subtype)) {
|
||||
if (subtype.literalValue !== undefined) {
|
||||
// If the object is already definitely truthy, it's fine to
|
||||
// include, otherwise it should be removed.
|
||||
return subtype.literalValue ? subtype : undefined;
|
||||
}
|
||||
|
||||
// If the object is a bool, make it "true", since
|
||||
// "false" is a falsy value.
|
||||
if (ClassType.isBuiltIn(subtype, 'bool')) {
|
||||
return ClassType.cloneWithLiteral(subtype, /* value */ true);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's possible for the type to be truthy, include it.
|
||||
if (canBeTruthy(subtype)) {
|
||||
return subtype;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Gets a member type from an object and if it's a function binds
|
||||
// it to the object. If bindToClass is undefined, the binding is done
|
||||
// using the objectType parameter. Callers can specify these separately
|
||||
@ -21173,6 +21361,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
||||
evaluateTypesForMatchNode,
|
||||
evaluateTypesForCaseNode,
|
||||
evaluateTypeOfParameter,
|
||||
canBeTruthy,
|
||||
canBeFalsy,
|
||||
removeTruthinessFromType,
|
||||
removeFalsinessFromType,
|
||||
verifyRaiseExceptionType,
|
||||
verifyDeleteExpression,
|
||||
isAfterNodeReachable,
|
||||
|
@ -251,6 +251,11 @@ export interface TypeEvaluator {
|
||||
evaluateTypesForCaseNode: (node: CaseNode) => void;
|
||||
evaluateTypeOfParameter: (node: ParameterNode) => void;
|
||||
|
||||
canBeTruthy: (type: Type) => boolean;
|
||||
canBeFalsy: (type: Type) => boolean;
|
||||
removeTruthinessFromType: (type: Type) => Type;
|
||||
removeFalsinessFromType: (type: Type) => Type;
|
||||
|
||||
getExpectedType: (node: ExpressionNode) => ExpectedTypeResult | undefined;
|
||||
verifyRaiseExceptionType: (node: RaiseNode) => void;
|
||||
verifyDeleteExpression: (node: ExpressionNode) => void;
|
||||
|
@ -73,6 +73,10 @@ export function createTypeEvaluatorWithTracker(
|
||||
evaluateTypesForMatchNode: typeEvaluator.evaluateTypesForMatchNode,
|
||||
evaluateTypesForCaseNode: typeEvaluator.evaluateTypesForCaseNode,
|
||||
evaluateTypeOfParameter: typeEvaluator.evaluateTypeOfParameter,
|
||||
canBeTruthy: typeEvaluator.canBeTruthy,
|
||||
canBeFalsy: typeEvaluator.canBeFalsy,
|
||||
removeTruthinessFromType: typeEvaluator.removeTruthinessFromType,
|
||||
removeFalsinessFromType: typeEvaluator.removeFalsinessFromType,
|
||||
getExpectedType: (n) => run('getExpectedType', () => typeEvaluator.getExpectedType(n), n),
|
||||
verifyRaiseExceptionType: (n) =>
|
||||
run('verifyRaiseExceptionType', () => typeEvaluator.verifyRaiseExceptionType(n), n),
|
||||
|
@ -48,8 +48,6 @@ import {
|
||||
import {
|
||||
addConditionToType,
|
||||
applySolvedTypeVars,
|
||||
canBeFalsy,
|
||||
canBeTruthy,
|
||||
ClassMember,
|
||||
computeMroLinearization,
|
||||
convertToInstance,
|
||||
@ -64,8 +62,6 @@ import {
|
||||
lookUpClassMember,
|
||||
lookUpObjectMember,
|
||||
mapSubtypes,
|
||||
removeFalsinessFromType,
|
||||
removeTruthinessFromType,
|
||||
transformPossibleRecursiveTypeAlias,
|
||||
} from './typeUtils';
|
||||
import { TypeVarMap } from './typeVarMap';
|
||||
@ -429,12 +425,12 @@ export function getTypeNarrowingCallback(
|
||||
// Narrow the type based on whether the subtype can be true or false.
|
||||
return mapSubtypes(type, (subtype) => {
|
||||
if (isPositiveTest) {
|
||||
if (canBeTruthy(subtype)) {
|
||||
return removeFalsinessFromType(subtype);
|
||||
if (evaluator.canBeTruthy(subtype)) {
|
||||
return evaluator.removeFalsinessFromType(subtype);
|
||||
}
|
||||
} else {
|
||||
if (canBeFalsy(subtype)) {
|
||||
return removeTruthinessFromType(subtype);
|
||||
if (evaluator.canBeFalsy(subtype)) {
|
||||
return evaluator.removeTruthinessFromType(subtype);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
@ -510,111 +510,6 @@ export function transformPossibleRecursiveTypeAlias(type: Type | undefined): Typ
|
||||
return type;
|
||||
}
|
||||
|
||||
// None is always falsy. All other types are generally truthy
|
||||
// unless they are objects that support the __bool__ or __len__
|
||||
// methods.
|
||||
export function canBeFalsy(type: Type, recursionLevel = 0): boolean {
|
||||
if (recursionLevel > maxTypeRecursionCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type.category) {
|
||||
case TypeCategory.Unbound:
|
||||
case TypeCategory.Unknown:
|
||||
case TypeCategory.Any:
|
||||
case TypeCategory.Never:
|
||||
case TypeCategory.None: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case TypeCategory.Union: {
|
||||
return findSubtype(type, (subtype) => canBeFalsy(subtype, recursionLevel + 1)) !== undefined;
|
||||
}
|
||||
|
||||
case TypeCategory.Function:
|
||||
case TypeCategory.OverloadedFunction:
|
||||
case TypeCategory.Module:
|
||||
case TypeCategory.TypeVar: {
|
||||
return false;
|
||||
}
|
||||
|
||||
case TypeCategory.Class: {
|
||||
if (TypeBase.isInstantiable(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle tuples specially.
|
||||
if (isTupleClass(type) && type.tupleTypeArguments) {
|
||||
return isOpenEndedTupleClass(type) || type.tupleTypeArguments.length === 0;
|
||||
}
|
||||
|
||||
// Check for Literal[False] and Literal[True].
|
||||
if (ClassType.isBuiltIn(type, 'bool') && type.literalValue !== undefined) {
|
||||
return type.literalValue === false;
|
||||
}
|
||||
|
||||
const lenMethod = lookUpObjectMember(type, '__len__');
|
||||
if (lenMethod) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const boolMethod = lookUpObjectMember(type, '__bool__');
|
||||
if (boolMethod) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function canBeTruthy(type: Type, recursionLevel = 0): boolean {
|
||||
if (recursionLevel > maxTypeRecursionCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type.category) {
|
||||
case TypeCategory.Unknown:
|
||||
case TypeCategory.Function:
|
||||
case TypeCategory.OverloadedFunction:
|
||||
case TypeCategory.Module:
|
||||
case TypeCategory.TypeVar:
|
||||
case TypeCategory.Never:
|
||||
case TypeCategory.Any: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case TypeCategory.Union: {
|
||||
return findSubtype(type, (subtype) => canBeTruthy(subtype, recursionLevel + 1)) !== undefined;
|
||||
}
|
||||
|
||||
case TypeCategory.Unbound:
|
||||
case TypeCategory.None: {
|
||||
return false;
|
||||
}
|
||||
|
||||
case TypeCategory.Class: {
|
||||
if (TypeBase.isInstantiable(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Tuple[()] (an empty tuple).
|
||||
if (isTupleClass(type)) {
|
||||
if (type.tupleTypeArguments && type.tupleTypeArguments!.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Literal[False], Literal[0], Literal[""].
|
||||
if (type.literalValue === false || type.literalValue === 0 || type.literalValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeVarScopeId(type: Type): TypeVarScopeId | undefined {
|
||||
if (isClass(type)) {
|
||||
return type.details.typeVarScopeId;
|
||||
@ -1419,65 +1314,6 @@ export function derivesFromClassRecursive(classType: ClassType, baseClassToFind:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filters a type such that that no part of it is definitely
|
||||
// falsy. For example, if a type is a union of None
|
||||
// and an "int", this method would strip off the "None"
|
||||
// and return only the "int".
|
||||
export function removeFalsinessFromType(type: Type): Type {
|
||||
return mapSubtypes(type, (subtype) => {
|
||||
if (isClassInstance(subtype)) {
|
||||
if (subtype.literalValue !== undefined) {
|
||||
// If the object is already definitely truthy, it's fine to
|
||||
// include, otherwise it should be removed.
|
||||
return subtype.literalValue ? subtype : undefined;
|
||||
}
|
||||
|
||||
// If the object is a bool, make it "true", since
|
||||
// "false" is a falsy value.
|
||||
if (ClassType.isBuiltIn(subtype, 'bool')) {
|
||||
return ClassType.cloneWithLiteral(subtype, /* value */ true);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's possible for the type to be truthy, include it.
|
||||
if (canBeTruthy(subtype)) {
|
||||
return subtype;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Filters a type such that that no part of it is definitely
|
||||
// truthy. For example, if a type is a union of None
|
||||
// and a custom class "Foo" that has no __len__ or __nonzero__
|
||||
// method, this method would strip off the "Foo"
|
||||
// and return only the "None".
|
||||
export function removeTruthinessFromType(type: Type): Type {
|
||||
return mapSubtypes(type, (subtype) => {
|
||||
if (isClassInstance(subtype)) {
|
||||
if (subtype.literalValue !== undefined) {
|
||||
// If the object is already definitely falsy, it's fine to
|
||||
// include, otherwise it should be removed.
|
||||
return !subtype.literalValue ? subtype : undefined;
|
||||
}
|
||||
|
||||
// If the object is a bool, make it "false", since
|
||||
// "true" is a truthy value.
|
||||
if (ClassType.isBuiltIn(subtype, 'bool')) {
|
||||
return ClassType.cloneWithLiteral(subtype, /* value */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's possible for the type to be falsy, include it.
|
||||
if (canBeFalsy(subtype)) {
|
||||
return subtype;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function synthesizeTypeVarForSelfCls(classType: ClassType, isClsParam: boolean): TypeVarType {
|
||||
const selfType = TypeVarType.createInstance(`__type_of_self__`);
|
||||
const scopeId = getTypeVarScopeId(classType) ?? '';
|
||||
|
@ -0,0 +1,29 @@
|
||||
# This sample tests type narrowing for falsy and truthy values.
|
||||
|
||||
from typing import List, Literal, Union
|
||||
|
||||
|
||||
class A:
|
||||
...
|
||||
|
||||
|
||||
class B:
|
||||
def __bool__(self) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class C:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
...
|
||||
|
||||
|
||||
class D:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
...
|
||||
|
||||
|
||||
def func1(x: Union[int, List[int], A, B, C, D, None]) -> None:
|
||||
if x:
|
||||
t1: Literal["int | List[int] | A | B | D"] = reveal_type(x)
|
||||
else:
|
||||
t2: Literal["int | List[int] | B | C | None"] = reveal_type(x)
|
@ -403,6 +403,12 @@ test('typeNarrowingCallable1', () => {
|
||||
TestUtils.validateResults(analysisResults, 2);
|
||||
});
|
||||
|
||||
test('TypeNarrowingFalsy1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingFalsy1.py']);
|
||||
|
||||
TestUtils.validateResults(analysisResults, 0);
|
||||
});
|
||||
|
||||
test('ReturnTypes1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['returnTypes1.py']);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user