mirror of
https://github.com/microsoft/pyright.git
synced 2024-11-03 21:30:08 +03:00
Fixed inconsistency in type narrowing for isinstance
and isclass
calls. Previously, the narrowing logic used the target class(es) if the source expression was of type Any but did not do the same when the source expression was a union type that included Any but all other subtypes were eliminated.
This commit is contained in:
parent
2efdd9b2df
commit
4634309d2a
@ -12996,42 +12996,48 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
||||
return filteredTypes.map((t) => ObjectType.create(t));
|
||||
};
|
||||
|
||||
if (isInstanceCheck && isObject(effectiveType)) {
|
||||
const filteredType = filterType(effectiveType.classType);
|
||||
return combineTypes(filteredType);
|
||||
} else if (!isInstanceCheck && isClass(effectiveType)) {
|
||||
const filteredType = filterType(effectiveType);
|
||||
return combineTypes(filteredType);
|
||||
} else if (effectiveType.category === TypeCategory.Union) {
|
||||
let remainingTypes: Type[] = [];
|
||||
const anyOrUnknownSubstitutions: Type[] = [];
|
||||
const anyOrUnknown: Type[] = [];
|
||||
|
||||
effectiveType.subtypes.forEach((t) => {
|
||||
if (isAnyOrUnknown(t)) {
|
||||
// Any types always remain for both positive and negative
|
||||
// checks because we can't say anything about them.
|
||||
remainingTypes.push(t);
|
||||
} else if (isInstanceCheck && isObject(t)) {
|
||||
remainingTypes = remainingTypes.concat(filterType(t.classType));
|
||||
} else if (!isInstanceCheck && isClass(t)) {
|
||||
remainingTypes = remainingTypes.concat(filterType(t));
|
||||
const filteredType = doForSubtypes(effectiveType, (subtype) => {
|
||||
if (isInstanceCheck && isObject(subtype)) {
|
||||
return combineTypes(filterType(subtype.classType));
|
||||
} else if (!isInstanceCheck && isClass(subtype)) {
|
||||
return combineTypes(filterType(subtype));
|
||||
} else if (isPositiveTest && isAnyOrUnknown(subtype)) {
|
||||
// If this is a positive test and the effective type is Any or
|
||||
// Unknown, we can assume that the type matches one of the
|
||||
// specified types.
|
||||
if (isInstanceCheck) {
|
||||
anyOrUnknownSubstitutions.push(
|
||||
combineTypes(classTypeList.map((classType) => ObjectType.create(classType)))
|
||||
);
|
||||
} else {
|
||||
// All other types are never instances of a class.
|
||||
if (!isPositiveTest) {
|
||||
remainingTypes.push(t);
|
||||
anyOrUnknownSubstitutions.push(combineTypes(classTypeList));
|
||||
}
|
||||
|
||||
anyOrUnknown.push(subtype);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return isPositiveTest ? undefined : subtype;
|
||||
});
|
||||
|
||||
return combineTypes(remainingTypes);
|
||||
} else if (isInstanceCheck && isPositiveTest && isAnyOrUnknown(effectiveType)) {
|
||||
// If this is a positive test for isinstance and the effective
|
||||
// type is Any or Unknown, we can assume that the type matches
|
||||
// one of the specified types.
|
||||
type = combineTypes(classTypeList.map((classType) => ObjectType.create(classType)));
|
||||
// If the result is Any/Unknown and contains no other subtypes and
|
||||
// we have substitutions for Any/Unknown, use those instead. We don't
|
||||
// want to apply this if the filtering produced something other than
|
||||
// Any/Unknown. For example, if the statement is "isinstance(x, list)"
|
||||
// and the type of x is "List[str] | int | Any", the result should be
|
||||
// "List[str]", not "List[str] | List[Unknown]".
|
||||
if (isNever(filteredType) && anyOrUnknownSubstitutions.length > 0) {
|
||||
return combineTypes(anyOrUnknownSubstitutions);
|
||||
}
|
||||
|
||||
// Return the original type.
|
||||
return type;
|
||||
if (anyOrUnknown.length > 0) {
|
||||
return combineTypes([filteredType, ...anyOrUnknown]);
|
||||
}
|
||||
|
||||
return filteredType;
|
||||
}
|
||||
|
||||
// Attempts to narrow a type (make it more constrained) based on an "in" or
|
||||
|
27
packages/pyright-internal/src/tests/samples/isinstance1.py
Normal file
27
packages/pyright-internal/src/tests/samples/isinstance1.py
Normal file
@ -0,0 +1,27 @@
|
||||
# This sample tests basic type narrowing behavior for
|
||||
# the isinstance call.
|
||||
|
||||
from typing import Any, List, Literal, Union
|
||||
|
||||
|
||||
def func1(x: Union[List[str], int]):
|
||||
if isinstance(x, list):
|
||||
t1: Literal["List[str]"] = reveal_type(x)
|
||||
else:
|
||||
t2: Literal["int"] = reveal_type(x)
|
||||
|
||||
|
||||
def func2(x: Any):
|
||||
if isinstance(x, list):
|
||||
t1: Literal["list[Unknown]"] = reveal_type(x)
|
||||
else:
|
||||
t2: Literal["Any"] = reveal_type(x)
|
||||
|
||||
|
||||
def func3(x):
|
||||
if isinstance(x, list):
|
||||
t1: Literal["list[Unknown]"] = reveal_type(x)
|
||||
else:
|
||||
t2: Literal["Unknown"] = reveal_type(x)
|
||||
|
||||
|
@ -102,6 +102,12 @@ test('NewType3', () => {
|
||||
TestUtils.validateResults(analysisResults, 4);
|
||||
});
|
||||
|
||||
test('isInstance1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['isinstance1.py']);
|
||||
|
||||
TestUtils.validateResults(analysisResults, 0);
|
||||
});
|
||||
|
||||
test('isInstance2', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['isinstance2.py']);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user