function canAssignFunction()

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;
    }