in packages/pyright-internal/src/analyzer/typeGuards.ts [831:1215]
function narrowTypeForIsInstance(
evaluator: TypeEvaluator,
type: Type,
classTypeList: (ClassType | TypeVarType | NoneType | FunctionType)[],
isInstanceCheck: boolean,
isPositiveTest: boolean,
allowIntersections: boolean,
errorNode: ExpressionNode
): Type {
const expandedTypes = mapSubtypes(type, (subtype) => {
return transformPossibleRecursiveTypeAlias(subtype);
});
// Filters the varType by the parameters of the isinstance
// and returns the list of types the varType could be after
// applying the filter.
const filterClassType = (
varType: ClassType,
unexpandedType: Type,
constraints: TypeCondition[] | undefined,
negativeFallbackType: Type
): Type[] => {
const filteredTypes: Type[] = [];
let foundSuperclass = false;
let isClassRelationshipIndeterminate = false;
for (const filterType of classTypeList) {
const concreteFilterType = evaluator.makeTopLevelTypeVarsConcrete(filterType);
if (isInstantiableClass(concreteFilterType)) {
// Handle the special case where the variable type is a TypedDict and
// we're filtering against 'dict'. TypedDict isn't derived from dict,
// but at runtime, isinstance returns True.
const filterIsSuperclass =
!isTypeVar(filterType) &&
(ClassType.isDerivedFrom(varType, concreteFilterType) ||
(isInstanceCheck &&
ClassType.isProtocolClass(concreteFilterType) &&
evaluator.canAssignType(concreteFilterType, varType)) ||
(ClassType.isBuiltIn(concreteFilterType, 'dict') && ClassType.isTypedDictClass(varType)));
const filterIsSubclass =
ClassType.isDerivedFrom(concreteFilterType, varType) ||
(isInstanceCheck &&
ClassType.isProtocolClass(varType) &&
evaluator.canAssignType(varType, concreteFilterType));
if (filterIsSuperclass) {
foundSuperclass = true;
}
// Normally, a type should never be both a subclass or a superclass.
// This can happen if either of the class types derives from a
// class whose type is unknown (e.g. an import failed). We'll
// note this case specially so we don't do any narrowing, which
// will generate false positives.
if (
filterIsSubclass &&
filterIsSuperclass &&
!ClassType.isSameGenericClass(varType, concreteFilterType)
) {
isClassRelationshipIndeterminate = true;
}
if (isPositiveTest) {
if (filterIsSuperclass) {
// If the variable type is a subclass of the isinstance filter,
// we haven't learned anything new about the variable type.
filteredTypes.push(addConditionToType(varType, constraints));
} else if (filterIsSubclass) {
// If the variable type is a superclass of the isinstance
// filter, we can narrow the type to the subclass.
let specializedFilterType = filterType;
// Try to retain the type arguments for the filter type. This is
// important because a specialized version of the filter cannot
// be passed to isinstance or issubclass.
if (isClass(filterType)) {
if (
ClassType.isSpecialBuiltIn(filterType) ||
filterType.details.typeParameters.length > 0
) {
const typeVarMap = new TypeVarMap(getTypeVarScopeId(filterType));
const unspecializedFilterType = ClassType.cloneForSpecialization(
filterType,
/* typeArguments */ undefined,
/* isTypeArgumentExplicit */ false
);
if (
evaluator.populateTypeVarMapBasedOnExpectedType(
unspecializedFilterType,
varType,
typeVarMap,
/* liveTypeVarScopes */ undefined
)
) {
specializedFilterType = applySolvedTypeVars(
unspecializedFilterType,
typeVarMap,
/* unknownIfNotFound */ true
) as ClassType;
}
}
}
filteredTypes.push(addConditionToType(specializedFilterType, constraints));
} else if (allowIntersections) {
// The two types appear to have no relation. It's possible that the
// two types are protocols or the program is expecting one type to
// be a mix-in class used with the other. In this case, we'll
// synthesize a new class type that represents an intersection of
// the two types.
const className = `<subclass of ${varType.details.name} and ${concreteFilterType.details.name}>`;
const fileInfo = getFileInfo(errorNode);
let newClassType = ClassType.createInstantiable(
className,
ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className),
fileInfo.moduleName,
fileInfo.filePath,
ClassTypeFlags.None,
ParseTreeUtils.getTypeSourceId(errorNode),
/* declaredMetaclass */ undefined,
varType.details.effectiveMetaclass,
varType.details.docString
);
newClassType.details.baseClasses = [ClassType.cloneAsInstantiable(varType), concreteFilterType];
computeMroLinearization(newClassType);
newClassType = addConditionToType(newClassType, concreteFilterType.condition) as ClassType;
if (
isTypeVar(unexpandedType) &&
!unexpandedType.details.isParamSpec &&
unexpandedType.details.constraints.length === 0
) {
newClassType = addConditionToType(newClassType, [
{
typeVarName: TypeVarType.getNameWithScope(unexpandedType),
constraintIndex: 0,
isConstrainedTypeVar: false,
},
]) as ClassType;
}
filteredTypes.push(isInstanceCheck ? ClassType.cloneAsInstance(newClassType) : newClassType);
}
}
} else if (isTypeVar(filterType) && TypeBase.isInstantiable(filterType)) {
// Handle the case where the filter type is Type[T] and the unexpanded
// subtype is some instance type, possibly T.
if (isInstanceCheck && TypeBase.isInstance(unexpandedType)) {
if (isTypeVar(unexpandedType) && isTypeSame(convertToInstance(filterType), unexpandedType)) {
// If the unexpanded subtype is T, we can definitively filter
// in both the positive and negative cases.
if (isPositiveTest) {
filteredTypes.push(unexpandedType);
}
} else {
if (isPositiveTest) {
filteredTypes.push(convertToInstance(filterType));
} else {
// If the unexpanded subtype is some other instance, we can't
// filter anything because it might be an instance.
filteredTypes.push(unexpandedType);
isClassRelationshipIndeterminate = true;
}
}
} else if (!isInstanceCheck && TypeBase.isInstantiable(unexpandedType)) {
if (isTypeVar(unexpandedType) && isTypeSame(filterType, unexpandedType)) {
if (isPositiveTest) {
filteredTypes.push(unexpandedType);
}
} else {
if (isPositiveTest) {
filteredTypes.push(filterType);
} else {
filteredTypes.push(unexpandedType);
isClassRelationshipIndeterminate = true;
}
}
}
} else if (isFunction(filterType)) {
// Handle an isinstance check against Callable.
if (isInstanceCheck) {
let isCallable = false;
if (isClass(varType)) {
if (TypeBase.isInstantiable(unexpandedType)) {
isCallable = true;
} else {
isCallable = !!lookUpClassMember(varType, '__call__');
}
}
if (isCallable) {
if (isPositiveTest) {
filteredTypes.push(unexpandedType);
} else {
foundSuperclass = true;
}
}
}
}
}
// In the negative case, if one or more of the filters
// always match the type (i.e. they are an exact match or
// a superclass of the type), then there's nothing left after
// the filter is applied. If we didn't find any superclass
// match, then the original variable type survives the filter.
if (!isPositiveTest) {
if (!foundSuperclass || isClassRelationshipIndeterminate) {
filteredTypes.push(negativeFallbackType);
}
}
if (!isInstanceCheck) {
return filteredTypes;
}
return filteredTypes.map((t) => convertToInstance(t));
};
const filterFunctionType = (varType: FunctionType | OverloadedFunctionType, unexpandedType: Type): Type[] => {
const filteredTypes: Type[] = [];
if (isPositiveTest) {
for (const filterType of classTypeList) {
const concreteFilterType = evaluator.makeTopLevelTypeVarsConcrete(filterType);
if (evaluator.canAssignType(varType, convertToInstance(concreteFilterType))) {
// If the filter type is a Callable, use the original type. If the
// filter type is a callback protocol, use the filter type.
if (isFunction(filterType)) {
filteredTypes.push(unexpandedType);
} else {
filteredTypes.push(convertToInstance(filterType));
}
}
}
} else if (
!classTypeList.some((filterType) => {
// If the filter type is a runtime checkable protocol class, it can
// be used in an instance check.
const concreteFilterType = evaluator.makeTopLevelTypeVarsConcrete(filterType);
if (isClass(concreteFilterType) && !ClassType.isProtocolClass(concreteFilterType)) {
return false;
}
return evaluator.canAssignType(varType, convertToInstance(concreteFilterType));
})
) {
filteredTypes.push(unexpandedType);
}
return filteredTypes;
};
const anyOrUnknownSubstitutions: Type[] = [];
const anyOrUnknown: Type[] = [];
const filteredType = evaluator.mapSubtypesExpandTypeVars(
expandedTypes,
/* conditionFilter */ undefined,
(subtype, unexpandedSubtype) => {
// If we fail to filter anything in the negative case, we need to decide
// whether to retain the original TypeVar or replace it with its specialized
// type(s). We'll assume that if someone is using isinstance or issubclass
// on a constrained TypeVar that they want to filter based on its constrained
// parts.
const negativeFallback = getTypeCondition(subtype) ? subtype : unexpandedSubtype;
const isSubtypeTypeObject = isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'type');
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) => convertToInstance(classType)))
);
} else {
anyOrUnknownSubstitutions.push(combineTypes(classTypeList));
}
anyOrUnknown.push(subtype);
return undefined;
}
if (isInstanceCheck) {
if (isNoneInstance(subtype)) {
const containsNoneType = classTypeList.some((t) => isNoneTypeClass(t));
if (isPositiveTest) {
return containsNoneType ? subtype : undefined;
} else {
return containsNoneType ? undefined : subtype;
}
}
if (isModule(subtype) || (isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'ModuleType'))) {
// Handle type narrowing for runtime-checkable protocols
// when applied to modules.
if (isPositiveTest) {
const filteredTypes = classTypeList.filter((classType) => {
const concreteClassType = evaluator.makeTopLevelTypeVarsConcrete(classType);
return (
isInstantiableClass(concreteClassType) && ClassType.isProtocolClass(concreteClassType)
);
});
if (filteredTypes.length > 0) {
return convertToInstance(combineTypes(filteredTypes));
}
}
}
if (isClassInstance(subtype) && !isSubtypeTypeObject) {
return combineTypes(
filterClassType(
ClassType.cloneAsInstantiable(subtype),
convertToInstance(unexpandedSubtype),
getTypeCondition(subtype),
negativeFallback
)
);
}
if ((isFunction(subtype) || isOverloadedFunction(subtype)) && isInstanceCheck) {
return combineTypes(filterFunctionType(subtype, convertToInstance(unexpandedSubtype)));
}
if (isInstantiableClass(subtype) || isSubtypeTypeObject) {
// Handle the special case of isinstance(x, type).
const includesTypeType = classTypeList.some(
(classType) => isInstantiableClass(classType) && ClassType.isBuiltIn(classType, 'type')
);
if (isPositiveTest) {
return includesTypeType ? negativeFallback : undefined;
} else {
return includesTypeType ? undefined : negativeFallback;
}
}
} else {
if (isInstantiableClass(subtype)) {
return combineTypes(
filterClassType(subtype, unexpandedSubtype, getTypeCondition(subtype), negativeFallback)
);
}
if (isSubtypeTypeObject) {
const objectType = evaluator.getBuiltInObject(errorNode, 'object');
if (objectType && isClassInstance(objectType)) {
return combineTypes(
filterClassType(
ClassType.cloneAsInstantiable(objectType),
convertToInstantiable(unexpandedSubtype),
getTypeCondition(subtype),
negativeFallback
)
);
}
}
}
return isPositiveTest ? undefined : negativeFallback;
}
);
// 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);
}
if (anyOrUnknown.length > 0) {
return combineTypes([filteredType, ...anyOrUnknown]);
}
return filteredType;
}