in packages/pyright-internal/src/analyzer/typeEvaluator.ts [21531:22116]
function canAssignFunction(
destType: FunctionType,
srcType: FunctionType,
diag: DiagnosticAddendum | undefined,
typeVarMap: TypeVarMap,
flags: CanAssignFlags,
recursionCount: number
): boolean {
let canAssign = true;
const checkReturnType = (flags & CanAssignFlags.SkipFunctionReturnTypeCheck) === 0;
flags &= ~CanAssignFlags.SkipFunctionReturnTypeCheck;
destType = removeParamSpecVariadicsFromFunction(destType);
srcType = removeParamSpecVariadicsFromFunction(srcType);
const destParamDetails = getParameterListDetails(destType);
const srcParamDetails = getParameterListDetails(srcType);
adjustSourceParamDetailsForDestVariadic(srcParamDetails, destParamDetails);
// The input typeVarMap normally corresponds to the destType, but it
// is reversed if the ReverseTypeVarMatching flag is set.
const destTypeVarMap =
(flags & CanAssignFlags.ReverseTypeVarMatching) === 0
? typeVarMap
: new TypeVarMap(getTypeVarScopeId(destType));
const srcTypeVarMap =
(flags & CanAssignFlags.ReverseTypeVarMatching) !== 0
? typeVarMap
: new TypeVarMap(getTypeVarScopeId(srcType));
const targetIncludesParamSpec =
(flags & CanAssignFlags.ReverseTypeVarMatching) !== 0
? !!srcType.details.paramSpec
: !!destType.details.paramSpec;
const destPositionalCount = destParamDetails.firstKeywordOnlyIndex ?? destParamDetails.params.length;
const srcPositionalCount = srcParamDetails.firstKeywordOnlyIndex ?? srcParamDetails.params.length;
const positionalsToMatch = Math.min(destPositionalCount, srcPositionalCount);
// Match positional parameters.
for (let paramIndex = 0; paramIndex < positionalsToMatch; paramIndex++) {
const destParam = destParamDetails.params[paramIndex];
const srcParam = srcParamDetails.params[paramIndex];
// Find the original index of this source param. If we synthesized it above (for
// a variadic parameter), it may not be found.
const srcParamType = srcParam.type;
const destParamType = destParam.type;
const destParamName = destParam.param.name ?? '';
const srcParamName = srcParam.param.name ?? '';
if (destParamName && !isPrivateOrProtectedName(destParamName) && !isPrivateOrProtectedName(srcParamName)) {
const isDestPositionalOnly = destParam.source === ParameterSource.PositionOnly;
if (
!isDestPositionalOnly &&
destParam.param.category !== ParameterCategory.VarArgList &&
srcParam.param.category !== ParameterCategory.VarArgList &&
destParamName !== srcParamName
) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.functionParamName().format({
srcName: srcParamName,
destName: destParamName,
})
);
}
canAssign = false;
}
}
if (!!destParam.param.hasDefault && !srcParam.param.hasDefault) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.functionParamDefaultMissing().format({
name: srcParamName,
})
);
}
canAssign = false;
}
// Handle the special case of an overloaded __init__ method whose self
// parameter is annotated.
if (
paramIndex === 0 &&
srcType.details.name === '__init__' &&
FunctionType.isInstanceMethod(srcType) &&
destType.details.name === '__init__' &&
FunctionType.isInstanceMethod(destType) &&
FunctionType.isOverloaded(destType) &&
destParam.param.hasDeclaredType
) {
continue;
}
if (
!canAssignFunctionParameter(
destParamType,
srcParamType,
paramIndex,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
// Handle the special case where the source parameter is a synthesized
// TypeVar for "self" or "cls".
if (
(flags & CanAssignFlags.SkipSelfClsTypeCheck) === 0 ||
!isTypeVar(srcParamType) ||
!srcParamType.details.isSynthesized
) {
canAssign = false;
}
}
}
if (
!FunctionType.shouldSkipArgsKwargsCompatibilityCheck(destType) &&
destParamDetails.firstPositionOrKeywordIndex < srcParamDetails.positionOnlyParamCount &&
!targetIncludesParamSpec
) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.argsPositionOnly().format({
expected: srcParamDetails.positionOnlyParamCount,
received: destParamDetails.firstPositionOrKeywordIndex,
})
);
}
canAssign = false;
}
if (destPositionalCount < srcPositionalCount) {
// If the dest type includes a ParamSpec, the additional parameters
// can be assigned to it, so no need to report an error here.
if (!targetIncludesParamSpec) {
const nonDefaultSrcParamCount = srcParamDetails.params.filter(
(p) => !!p.param.name && !p.param.hasDefault && p.param.category === ParameterCategory.Simple
).length;
if (destParamDetails.argsIndex === undefined) {
if (destPositionalCount < nonDefaultSrcParamCount) {
if (
destParamDetails.firstPositionOrKeywordIndex > 0 &&
destParamDetails.firstPositionOrKeywordIndex < srcPositionalCount
) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.functionTooFewParams().format({
expected: nonDefaultSrcParamCount,
received: destPositionalCount,
})
);
}
canAssign = false;
}
}
} else {
// Make sure the remaining positional arguments are of the
// correct type for the *args parameter.
const destArgsType = destParamDetails.params[destParamDetails.argsIndex].type;
if (!isAnyOrUnknown(destArgsType)) {
for (let paramIndex = destPositionalCount; paramIndex < srcPositionalCount; paramIndex++) {
const srcParamType = srcParamDetails.params[paramIndex].type;
if (
!canAssignFunctionParameter(
destArgsType,
srcParamType,
paramIndex,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
}
}
}
}
} else if (srcPositionalCount < destPositionalCount) {
if (srcParamDetails.argsIndex !== undefined) {
// Make sure the remaining dest parameters can be assigned to the source
// *args parameter type.
const srcArgsType = srcParamDetails.params[srcParamDetails.argsIndex].type;
for (let paramIndex = srcPositionalCount; paramIndex < destPositionalCount; paramIndex++) {
const destParamType = destParamDetails.params[paramIndex].type;
if (isVariadicTypeVar(destParamType) && !isVariadicTypeVar(srcArgsType)) {
if (diag) {
diag.addMessage(Localizer.DiagnosticAddendum.typeVarTupleRequiresKnownLength());
}
canAssign = false;
} else if (
!canAssignFunctionParameter(
destParamType,
srcArgsType,
paramIndex,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
}
} else {
if (diag) {
diag.addMessage(
Localizer.DiagnosticAddendum.functionTooManyParams().format({
expected: srcPositionalCount,
received: destPositionalCount,
})
);
}
canAssign = false;
}
}
// If both src and dest have an "*args" parameter, make sure
// their types are compatible.
if (srcParamDetails.argsIndex !== undefined && destParamDetails.argsIndex !== undefined) {
if (
!canAssignFunctionParameter(
destParamDetails.params[destParamDetails.argsIndex].type,
srcParamDetails.params[srcParamDetails.argsIndex].type,
destParamDetails.params[destParamDetails.argsIndex].index,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
}
// If the dest has an "*args" but the source doesn't, report the incompatibility.
// The converse situation is OK.
if (
!FunctionType.shouldSkipArgsKwargsCompatibilityCheck(destType) &&
srcParamDetails.argsIndex === undefined &&
destParamDetails.argsIndex !== undefined &&
!destParamDetails.hasUnpackedVariadicTypeVar &&
!targetIncludesParamSpec
) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.argsParamMissing().format({
paramName: destParamDetails.params[destParamDetails.argsIndex].param.name ?? '',
})
);
}
canAssign = false;
}
// Handle matching of named (keyword) parameters.
if (!targetIncludesParamSpec) {
// Build a dictionary of named parameters in the dest.
const destParamMap = new Map<string, VirtualParameterDetails>();
if (destParamDetails.firstKeywordOnlyIndex !== undefined) {
destParamDetails.params.forEach((param, index) => {
if (index >= destParamDetails.firstKeywordOnlyIndex!) {
if (param.param.name && param.param.category === ParameterCategory.Simple) {
destParamMap.set(param.param.name, param);
}
}
});
}
// If the dest has fewer positional arguments than the source, the remaining
// positional arguments in the source can be treated as named arguments.
let srcStartOfNamed =
srcParamDetails.firstKeywordOnlyIndex !== undefined
? srcParamDetails.firstKeywordOnlyIndex
: srcParamDetails.params.length;
if (destPositionalCount < srcPositionalCount && destParamDetails.argsIndex === undefined) {
srcStartOfNamed = destPositionalCount;
}
if (srcStartOfNamed >= 0) {
srcParamDetails.params.forEach((srcParamInfo, index) => {
if (index >= srcStartOfNamed) {
if (srcParamInfo.param.name && srcParamInfo.param.category === ParameterCategory.Simple) {
const destParamInfo = destParamMap.get(srcParamInfo.param.name);
const paramDiag = diag?.createAddendum();
const srcParamType = FunctionType.getEffectiveParameterType(srcType, srcParamInfo.index);
if (!destParamInfo) {
if (destParamDetails.kwargsIndex === undefined && !srcParamInfo.param.hasDefault) {
if (paramDiag) {
paramDiag.addMessage(
Localizer.DiagnosticAddendum.namedParamMissingInDest().format({
name: srcParamInfo.param.name,
})
);
}
canAssign = false;
} else if (destParamDetails.kwargsIndex !== undefined) {
// Make sure we can assign the type to the Kwargs.
if (
!canAssignFunctionParameter(
destParamDetails.params[destParamDetails.kwargsIndex].type,
srcParamType,
destParamDetails.params[destParamDetails.kwargsIndex].index,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
}
} else {
const destParamType = FunctionType.getEffectiveParameterType(
destType,
destParamInfo.index
);
const specializedDestParamType = destTypeVarMap
? applySolvedTypeVars(destParamType, destTypeVarMap)
: destParamType;
if (
!canAssignType(
srcParamType,
specializedDestParamType,
paramDiag?.createAddendum(),
undefined,
flags,
recursionCount
)
) {
if (paramDiag) {
paramDiag.addMessage(
Localizer.DiagnosticAddendum.namedParamTypeMismatch().format({
name: srcParamInfo.param.name,
sourceType: printType(specializedDestParamType),
destType: printType(srcParamType),
})
);
}
canAssign = false;
}
if (!!destParamInfo.param.hasDefault && !srcParamInfo.param.hasDefault) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.functionParamDefaultMissing().format({
name: srcParamInfo.param.name,
})
);
}
canAssign = false;
}
destParamMap.delete(srcParamInfo.param.name);
}
}
}
});
}
// See if there are any unmatched named parameters.
destParamMap.forEach((destParamInfo, paramName) => {
if (srcParamDetails.kwargsIndex !== undefined && destParamInfo.param.name) {
// Make sure the src kwargs type is compatible.
if (
!canAssignFunctionParameter(
destParamInfo.param.type,
srcParamDetails.params[srcParamDetails.kwargsIndex].type,
destParamInfo.index,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
destParamMap.delete(paramName);
} else {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.namedParamMissingInSource().format({ name: paramName })
);
}
canAssign = false;
}
});
// If both src and dest have a "*kwargs" parameter, make sure their types are compatible.
if (srcParamDetails.kwargsIndex !== undefined && destParamDetails.kwargsIndex !== undefined) {
if (
!canAssignFunctionParameter(
destParamDetails.params[destParamDetails.kwargsIndex].type,
srcParamDetails.params[srcParamDetails.kwargsIndex].type,
destParamDetails.params[destParamDetails.kwargsIndex].index,
diag?.createAddendum(),
destTypeVarMap,
srcTypeVarMap,
flags,
recursionCount
)
) {
canAssign = false;
}
}
// If the dest has a "**kwargs" but the source doesn't, report the incompatibility.
// The converse situation is OK.
if (
!FunctionType.shouldSkipArgsKwargsCompatibilityCheck(destType) &&
srcParamDetails.kwargsIndex === undefined &&
destParamDetails.kwargsIndex !== undefined
) {
if (diag) {
diag.createAddendum().addMessage(
Localizer.DiagnosticAddendum.kwargsParamMissing().format({
paramName: destParamDetails.params[destParamDetails.kwargsIndex].param.name!,
})
);
}
canAssign = false;
}
}
// If the source and the dest are using the same ParamSpec, any additional
// concatenated parameters must match.
if (
targetIncludesParamSpec &&
srcType.details.paramSpec?.nameWithScope === destType.details.paramSpec?.nameWithScope
) {
const srcParamCount = srcType.details.parameters.length;
const destParamCount = destType.details.parameters.length;
if (srcParamCount !== destParamCount) {
// If the dest has an extra position-only parameter separator appended
// to the end of the signature, it's OK.
if (
srcParamCount !== destParamCount - 1 ||
destType.details.parameters[destParamCount - 1].category !== ParameterCategory.Simple ||
!!destType.details.parameters[destParamCount - 1].name
) {
canAssign = false;
}
}
}
if (typeVarMap && !typeVarMap.isLocked()) {
const effectiveSrcTypeVarMap =
(flags & CanAssignFlags.ReverseTypeVarMatching) === 0 ? srcTypeVarMap : destTypeVarMap;
// If the target function was generic and we solved some of the type variables
// in that generic type, assign them back to the destination typeVar.
effectiveSrcTypeVarMap.getTypeVars().forEach((typeVarEntry) => {
canAssignType(
typeVarEntry.typeVar,
effectiveSrcTypeVarMap.getTypeVarType(typeVarEntry.typeVar)!,
/* diag */ undefined,
typeVarMap,
/* flags */ undefined,
recursionCount
);
});
// Perform partial specialization of type variables to allow for
// "higher-order" type variables.
typeVarMap.getTypeVars().forEach((entry) => {
if (entry.narrowBound) {
const specializedType = applySolvedTypeVars(entry.narrowBound, typeVarMap);
if (specializedType !== entry.narrowBound) {
typeVarMap.setTypeVarType(entry.typeVar, specializedType, entry.wideBound, entry.retainLiteral);
}
}
});
// Are we assigning to a function with a ParamSpec?
if (targetIncludesParamSpec) {
const effectiveDestType = (flags & CanAssignFlags.ReverseTypeVarMatching) === 0 ? destType : srcType;
const effectiveSrcType = (flags & CanAssignFlags.ReverseTypeVarMatching) === 0 ? srcType : destType;
if (effectiveDestType.details.paramSpec) {
typeVarMap.setParamSpec(effectiveDestType.details.paramSpec, {
parameters: effectiveSrcType.details.parameters
.map((p, index) => {
const paramSpecEntry: ParamSpecEntry = {
category: p.category,
name: p.name,
isNameSynthesized: p.isNameSynthesized,
hasDefault: !!p.hasDefault,
type: FunctionType.getEffectiveParameterType(effectiveSrcType, index),
};
return paramSpecEntry;
})
.slice(
// Skip position-only and keyword-only separators.
effectiveDestType.details.parameters.filter((p) => p.name).length,
effectiveSrcType.details.parameters.length
),
typeVarScopeId: effectiveSrcType.details.typeVarScopeId,
docString: effectiveSrcType.details.docString,
flags: effectiveSrcType.details.flags,
paramSpec: effectiveSrcType.details.paramSpec
? (convertToInstance(effectiveSrcType.details.paramSpec) as TypeVarType)
: undefined,
});
}
}
}
// Match the return parameter.
if (checkReturnType) {
const destReturnType = getFunctionEffectiveReturnType(destType);
if (!isAnyOrUnknown(destReturnType)) {
const srcReturnType = applySolvedTypeVars(getFunctionEffectiveReturnType(srcType), srcTypeVarMap);
const returnDiag = diag?.createAddendum();
let isReturnTypeCompatible = false;
if (isNoReturnType(srcReturnType)) {
// We'll allow any function that returns NoReturn to match any
// function return type, consistent with other type checkers.
isReturnTypeCompatible = true;
} else if (
canAssignType(
destReturnType,
srcReturnType,
returnDiag?.createAddendum(),
typeVarMap,
flags,
recursionCount
)
) {
isReturnTypeCompatible = true;
} else {
// Handle the special case where the return type is a TypeGuard[T].
// This should also act as a bool, since that's its type at runtime.
if (
isClassInstance(srcReturnType) &&
ClassType.isBuiltIn(srcReturnType, ['TypeGuard', 'StrictTypeGuard']) &&
boolClassType &&
isInstantiableClass(boolClassType)
) {
if (
canAssignType(
destReturnType,
ClassType.cloneAsInstance(boolClassType),
returnDiag?.createAddendum(),
typeVarMap,
flags,
recursionCount
)
) {
isReturnTypeCompatible = true;
}
}
}
if (!isReturnTypeCompatible) {
if (returnDiag) {
returnDiag.addMessage(
Localizer.DiagnosticAddendum.functionReturnTypeMismatch().format({
sourceType: printType(srcReturnType),
destType: printType(destReturnType),
})
);
}
canAssign = false;
}
}
}
return canAssign;
}