mirror of
https://github.com/microsoft/pyright.git
synced 2024-11-13 09:34:44 +03:00
Improved handling of bidirectional type inference for constructor calls on generic types. In particular, the new logic better handles the case where the expected type is a union.
This commit is contained in:
parent
9a991dc8d3
commit
a42d075b90
@ -5493,43 +5493,48 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
||||
return type;
|
||||
}
|
||||
|
||||
// It's common for the expected type to contain a None. Strip
|
||||
// this out because we're trying to match the non-optional part.
|
||||
const expectedTypeWithoutNone = removeNoneFromUnion(expectedType);
|
||||
if (!isObject(expectedTypeWithoutNone)) {
|
||||
return type;
|
||||
}
|
||||
const expectedClass = expectedTypeWithoutNone.classType;
|
||||
// Try to apply for each subtype in the expectedType. The foundMatch
|
||||
// tracks whether we've already seen a match. If none of them match,
|
||||
// return the original type.
|
||||
let foundMatch = false;
|
||||
const specializedType = doForSubtypes(expectedType, (subtype) => {
|
||||
if (foundMatch || !isObject(subtype)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const typeVarMap = new TypeVarMap();
|
||||
if (canAssignType(expectedClass, type, new DiagnosticAddendum(), typeVarMap)) {
|
||||
return specializeType(expectedClass, typeVarMap) as ClassType;
|
||||
}
|
||||
|
||||
// If it's the same generic class, see if we can assign the type arguments
|
||||
// without the variance rules that canAssignType uses.
|
||||
if (
|
||||
ClassType.isSameGenericClass(type, expectedClass) &&
|
||||
expectedClass.typeArguments &&
|
||||
type.typeArguments &&
|
||||
!type.isTypeArgumentExplicit &&
|
||||
expectedClass.typeArguments.length === type.typeArguments.length
|
||||
) {
|
||||
const expectedClass = subtype.classType;
|
||||
const typeVarMap = new TypeVarMap();
|
||||
let isAssignable = true;
|
||||
expectedClass.typeArguments.forEach((expectedTypeArg, index) => {
|
||||
const typeTypeArg = type.typeArguments![index];
|
||||
if (!canAssignType(expectedTypeArg, typeTypeArg, new DiagnosticAddendum(), typeVarMap)) {
|
||||
isAssignable = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isAssignable) {
|
||||
if (canAssignType(expectedClass, type, new DiagnosticAddendum(), typeVarMap)) {
|
||||
foundMatch = true;
|
||||
return specializeType(expectedClass, typeVarMap) as ClassType;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
// If it's the same generic class, see if we can assign the type arguments
|
||||
// without the variance rules that canAssignType uses.
|
||||
if (
|
||||
ClassType.isSameGenericClass(type, expectedClass) &&
|
||||
expectedClass.typeArguments &&
|
||||
type.typeArguments &&
|
||||
!type.isTypeArgumentExplicit &&
|
||||
expectedClass.typeArguments.length === type.typeArguments.length
|
||||
) {
|
||||
const typeVarMap = new TypeVarMap();
|
||||
let isAssignable = true;
|
||||
expectedClass.typeArguments.forEach((expectedTypeArg, index) => {
|
||||
const typeTypeArg = type.typeArguments![index];
|
||||
if (!canAssignType(expectedTypeArg, typeTypeArg, new DiagnosticAddendum(), typeVarMap)) {
|
||||
isAssignable = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isAssignable) {
|
||||
foundMatch = true;
|
||||
return specializeType(expectedClass, typeVarMap) as ClassType;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return isClass(specializedType) ? specializedType : type;
|
||||
}
|
||||
|
||||
// In cases where the expected type is a specialized base class of the
|
||||
@ -5538,73 +5543,82 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
||||
// performs this reverse mapping of type arguments and populates the type var
|
||||
// map for the target type.
|
||||
function populateTypeVarMapBasedOnExpectedType(type: ObjectType, expectedType: Type, typeVarMap: TypeVarMap) {
|
||||
// It's common for the expected type to be Optional. Remove the None
|
||||
// to see if the resulting type is an object.
|
||||
const expectedTypeWithoutNone = removeNoneFromUnion(expectedType);
|
||||
|
||||
// If the resulting type isn't an object, we can't proceed.
|
||||
if (!isObject(expectedTypeWithoutNone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the target type isn't generic, there's nothing for us to do.
|
||||
if (!requiresSpecialization(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the expected type is generic (not specialized), we can't proceed.
|
||||
const expectedTypeArgs =
|
||||
expectedTypeWithoutNone.classType.effectiveTypeArguments || expectedTypeWithoutNone.classType.typeArguments;
|
||||
if (expectedTypeArgs === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the expected type is the same as the target type (commonly the case),
|
||||
// we can use a faster method.
|
||||
if (ClassType.isSameGenericClass(expectedTypeWithoutNone.classType, type.classType)) {
|
||||
canAssignType(type, expectedTypeWithoutNone, new DiagnosticAddendum(), typeVarMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a generic (not specialized) version of the expected type.
|
||||
const genericExpectedType = ClassType.cloneForSpecialization(
|
||||
expectedTypeWithoutNone.classType,
|
||||
undefined,
|
||||
/* isTypeArgumentExplicit */ false
|
||||
);
|
||||
|
||||
// For each type param in the target type, create a placeholder type variable.
|
||||
const typeArgs = type.classType.details.typeParameters.map((_, index) => {
|
||||
const typeVar = TypeVarType.createInstance(`__${index}`, /* isParamSpec */ false, /* isSynthesized */ true);
|
||||
typeVar.details.synthesizedIndex = index;
|
||||
return typeVar;
|
||||
});
|
||||
|
||||
const specializedType = ClassType.cloneForSpecialization(
|
||||
type.classType,
|
||||
typeArgs,
|
||||
/* isTypeArgumentExplicit */ true
|
||||
);
|
||||
const syntheticTypeVarMap = new TypeVarMap();
|
||||
if (canAssignType(genericExpectedType, specializedType, new DiagnosticAddendum(), syntheticTypeVarMap)) {
|
||||
genericExpectedType.details.typeParameters.forEach((typeVar, index) => {
|
||||
const synthTypeVar = syntheticTypeVarMap.getTypeVar(typeVar);
|
||||
|
||||
// Is this one of the synthesized type vars we allocated above? If so,
|
||||
// the type arg that corresponds to this type var maps back to the target type.
|
||||
if (
|
||||
synthTypeVar &&
|
||||
isTypeVar(synthTypeVar) &&
|
||||
synthTypeVar.details.isSynthesized &&
|
||||
synthTypeVar.details.synthesizedIndex !== undefined
|
||||
) {
|
||||
const targetTypeVar = specializedType.details.typeParameters[synthTypeVar.details.synthesizedIndex];
|
||||
if (index < expectedTypeArgs.length) {
|
||||
typeVarMap.setTypeVar(targetTypeVar, expectedTypeArgs[index], /* isNarrowable */ false);
|
||||
}
|
||||
// Try to find a subtype within the expected type that the type can be assigned to.
|
||||
// If found, fill in the typeVarMap with the required specialization type arguments.
|
||||
let foundMatch = false;
|
||||
doForSubtypes(expectedType, (subtype) => {
|
||||
if (!foundMatch && isObject(subtype)) {
|
||||
// If the expected type is generic (not specialized), we can't proceed.
|
||||
const expectedTypeArgs = subtype.classType.effectiveTypeArguments || subtype.classType.typeArguments;
|
||||
if (expectedTypeArgs === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the expected type is the same as the target type (commonly the case),
|
||||
// we can use a faster method.
|
||||
if (ClassType.isSameGenericClass(subtype.classType, type.classType)) {
|
||||
canAssignType(type, subtype, new DiagnosticAddendum(), typeVarMap);
|
||||
foundMatch = true;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create a generic (not specialized) version of the expected type.
|
||||
const genericExpectedType = ClassType.cloneForSpecialization(
|
||||
subtype.classType,
|
||||
undefined,
|
||||
/* isTypeArgumentExplicit */ false
|
||||
);
|
||||
|
||||
// For each type param in the target type, create a placeholder type variable.
|
||||
const typeArgs = type.classType.details.typeParameters.map((_, index) => {
|
||||
const typeVar = TypeVarType.createInstance(
|
||||
`__${index}`,
|
||||
/* isParamSpec */ false,
|
||||
/* isSynthesized */ true
|
||||
);
|
||||
typeVar.details.synthesizedIndex = index;
|
||||
return typeVar;
|
||||
});
|
||||
|
||||
const specializedType = ClassType.cloneForSpecialization(
|
||||
type.classType,
|
||||
typeArgs,
|
||||
/* isTypeArgumentExplicit */ true
|
||||
);
|
||||
const syntheticTypeVarMap = new TypeVarMap();
|
||||
if (
|
||||
canAssignType(genericExpectedType, specializedType, new DiagnosticAddendum(), syntheticTypeVarMap)
|
||||
) {
|
||||
genericExpectedType.details.typeParameters.forEach((typeVar, index) => {
|
||||
const synthTypeVar = syntheticTypeVarMap.getTypeVar(typeVar);
|
||||
|
||||
// Is this one of the synthesized type vars we allocated above? If so,
|
||||
// the type arg that corresponds to this type var maps back to the target type.
|
||||
if (
|
||||
synthTypeVar &&
|
||||
isTypeVar(synthTypeVar) &&
|
||||
synthTypeVar.details.isSynthesized &&
|
||||
synthTypeVar.details.synthesizedIndex !== undefined
|
||||
) {
|
||||
const targetTypeVar =
|
||||
specializedType.details.typeParameters[synthTypeVar.details.synthesizedIndex];
|
||||
if (index < expectedTypeArgs.length) {
|
||||
typeVarMap.setTypeVar(targetTypeVar, expectedTypeArgs[index], /* isNarrowable */ false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Validates that the arguments can be assigned to the call's parameter
|
||||
|
@ -2327,3 +2327,9 @@ test('None1', () => {
|
||||
|
||||
validateResults(analysisResults, 1);
|
||||
});
|
||||
|
||||
test('Constructor1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['constructor1.py']);
|
||||
|
||||
validateResults(analysisResults, 0);
|
||||
});
|
||||
|
31
packages/pyright-internal/src/tests/samples/constructor1.py
Normal file
31
packages/pyright-internal/src/tests/samples/constructor1.py
Normal file
@ -0,0 +1,31 @@
|
||||
# This sample tests the handling of a constructor for a generic
|
||||
# class where the type arguments need to be inferred using
|
||||
# bidirectional type inference and the expected type is a
|
||||
# union of other types.
|
||||
|
||||
from typing import Generic, TypeVar, Union, Final, Optional
|
||||
|
||||
T = TypeVar("T")
|
||||
E = TypeVar("E")
|
||||
|
||||
|
||||
class Ok(Generic[T]):
|
||||
def __init__(self, value: T) -> None:
|
||||
self._value: Final = value
|
||||
|
||||
|
||||
class Err(Generic[E]):
|
||||
def __init__(self, value: E) -> None:
|
||||
self._value: Final = value
|
||||
|
||||
|
||||
Result = Union[Ok[T], Err[E]]
|
||||
|
||||
|
||||
def return_ok_none() -> Result[Optional[int], Exception]:
|
||||
return Ok(None)
|
||||
|
||||
|
||||
def return_ok_one() -> Result[Optional[int], Exception]:
|
||||
return Ok(1)
|
||||
|
Loading…
Reference in New Issue
Block a user