mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
Modified overload matching algorithm to better match that of mypy. In particular, if overload matching is ambiguous due to an Any
or Unknown
anywhere within an argument type (even within type arguments) and the resulting filtered overloads return the same generic type but with different type arguments, the resulting type was previously the return type of the first filtered overload. It is now the common generic type with erased type arguments. This addresses https://github.com/microsoft/pyright/issues/5216.
This commit is contained in:
parent
771b7e66bb
commit
746e03a49d
@ -307,7 +307,7 @@ Some functions or methods can return one of several different types. In cases wh
|
|||||||
|
|
||||||
4. If more than one overload remains, the “winner” is chosen based on the order in which the overloads are declared. In general, the first remaining overload is the “winner”. There are two exceptions to this rule.
|
4. If more than one overload remains, the “winner” is chosen based on the order in which the overloads are declared. In general, the first remaining overload is the “winner”. There are two exceptions to this rule.
|
||||||
Exception 1: When an `*args` (unpacked) argument matches a `*args` parameter in one of the overload signatures, this overrides the normal order-based rule.
|
Exception 1: When an `*args` (unpacked) argument matches a `*args` parameter in one of the overload signatures, this overrides the normal order-based rule.
|
||||||
Exception 2: When two or more overloads match because an argument evaluates to `Any` or `Unknown`, the matching overload is ambiguous. In this case, pyright examines the return types of the remaining overloads and eliminates types that are duplicates or are subsumed by (i.e. proper subtypes of) other types in the list. If only one type remains after this coalescing step, that type is used. If more than one type remains after this coalescing step, the type of the call expression evaluates to `Unknown`. For example, if two overloads are matched due to an argument that evaluates to `Any`, and those two overloads have return types of `str` and `LiteralString`, pyright will coalesce this to just `str` because `LiteralString` is a proper subtype of `str`. If the two overloads have return types of `str` and `bytes`, the call expression will evaluate to `Unknown` because `str` and `bytes` have no overlap.
|
Exception 2: When two or more overloads match because an argument or a type argument evaluates to `Any` or `Unknown` and this causes overload matching to be ambiguous. In this case, pyright examines the return types of the remaining overloads and eliminates types that are duplicates or are subsumed by (i.e. proper subtypes of) other types in the list. If only one type remains after this coalescing step, that type is used. If more than one type remains after this coalescing step and all of the types are the same class but with different type arguments, these type arguments are “erased” (i.e. replaced with `Unknown`). Otherwise, the type of the call expression evaluates to `Unknown`. For example, if two overloads are matched due to an argument that evaluates to `Any`, and those two overloads have return types of `str` and `LiteralString`, pyright will coalesce this to just `str` because `LiteralString` is a proper subtype of `str`. If the two overloads have return types of `list[str]` and `list[int]`, the call expression will evaluate to `list[Unknown]`. If the two overloads have return types of `str` and `bytes`, the call expression will evaluate to `Unknown` because `str` and `bytes` have no overlap.
|
||||||
|
|
||||||
5. If no overloads remain, Pyright considers whether any of the arguments are union types. If so, these union types are expanded into their constituent subtypes, and the entire process of overload matching is repeated with the expanded argument types. If two or more overloads match, the union of their respective return types form the final return type for the call expression.
|
5. If no overloads remain, Pyright considers whether any of the arguments are union types. If so, these union types are expanded into their constituent subtypes, and the entire process of overload matching is repeated with the expanded argument types. If two or more overloads match, the union of their respective return types form the final return type for the call expression.
|
||||||
|
|
||||||
|
@ -206,6 +206,7 @@ import {
|
|||||||
doForEachSubtype,
|
doForEachSubtype,
|
||||||
ensureFunctionSignaturesAreUnique,
|
ensureFunctionSignaturesAreUnique,
|
||||||
explodeGenericClass,
|
explodeGenericClass,
|
||||||
|
getCommonErasedType,
|
||||||
getContainerDepth,
|
getContainerDepth,
|
||||||
getDeclaredGeneratorReturnType,
|
getDeclaredGeneratorReturnType,
|
||||||
getGeneratorTypeArgs,
|
getGeneratorTypeArgs,
|
||||||
@ -8256,10 +8257,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
if (dedupedResultsIncludeAny) {
|
if (dedupedResultsIncludeAny) {
|
||||||
effectiveReturnType = AnyType.create();
|
effectiveReturnType = AnyType.create();
|
||||||
} else {
|
} else {
|
||||||
effectiveReturnType = UnknownType.createPossibleType(
|
// If all of the return types are the same generic class,
|
||||||
combinedTypes,
|
// replace the type arguments with Unknown. Otherwise return
|
||||||
possibleMatchInvolvesIncompleteUnknown
|
// an Unknown type that has associated "possible types" to aid
|
||||||
);
|
// with completion suggestions.
|
||||||
|
effectiveReturnType =
|
||||||
|
getCommonErasedType(dedupedMatchResults) ??
|
||||||
|
UnknownType.createPossibleType(combinedTypes, possibleMatchInvolvesIncompleteUnknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8314,6 +8318,17 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the relevance of some matches differs, filter out the ones that
|
||||||
|
// are lower relevance. This favors *args parameters in cases where
|
||||||
|
// a *args argument is used.
|
||||||
|
if (matches[0].matchResults.relevance !== matches[matches.length - 1].matchResults.relevance) {
|
||||||
|
matches = matches.filter((m) => m.matchResults.relevance === matches[0].matchResults.relevance);
|
||||||
|
|
||||||
|
if (matches.length < 2) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If all of the return types match, select the first one.
|
// If all of the return types match, select the first one.
|
||||||
if (
|
if (
|
||||||
areTypesSame(
|
areTypesSame(
|
||||||
@ -8330,9 +8345,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < firstArgResults.length; i++) {
|
for (let i = 0; i < firstArgResults.length; i++) {
|
||||||
// If the arg is Any or Unknown, see if the corresponding
|
// If the arg contains Any or Unknown, see if the corresponding
|
||||||
// parameter types differ in any way.
|
// parameter types differ in any way.
|
||||||
if (isAnyOrUnknown(firstArgResults[i].argType)) {
|
const anyOrUnknownInArg = containsAnyOrUnknown(firstArgResults[i].argType, /* recurse */ true);
|
||||||
|
|
||||||
|
if (anyOrUnknownInArg) {
|
||||||
const paramTypes = matches.map((match) =>
|
const paramTypes = matches.map((match) =>
|
||||||
i < match.matchResults.argParams.length
|
i < match.matchResults.argParams.length
|
||||||
? match.matchResults.argParams[i].paramType
|
? match.matchResults.argParams[i].paramType
|
||||||
@ -10409,7 +10426,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
let isTypeIncomplete = matchResults.isTypeIncomplete;
|
let isTypeIncomplete = matchResults.isTypeIncomplete;
|
||||||
let argumentErrors = false;
|
let argumentErrors = false;
|
||||||
let specializedInitSelfType: Type | undefined;
|
let specializedInitSelfType: Type | undefined;
|
||||||
let anyOrUnknownArgument: UnknownType | AnyType | undefined;
|
let accumulatedAnyOrUnknownArg: UnknownType | AnyType | undefined;
|
||||||
const typeCondition = getTypeCondition(type);
|
const typeCondition = getTypeCondition(type);
|
||||||
|
|
||||||
if (type.boundTypeVarScopeId) {
|
if (type.boundTypeVarScopeId) {
|
||||||
@ -10564,10 +10581,14 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
condition = TypeCondition.combine(condition, argResult.condition) ?? [];
|
condition = TypeCondition.combine(condition, argResult.condition) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnyOrUnknown(argResult.argType)) {
|
// Determine if the argument type contains an Any or Unknown. Accumulate
|
||||||
anyOrUnknownArgument = anyOrUnknownArgument
|
// these across all arguments.
|
||||||
? preserveUnknown(argResult.argType, anyOrUnknownArgument)
|
const anyOrUnknownInArg = containsAnyOrUnknown(argResult.argType, /* recurs */ true);
|
||||||
: argResult.argType;
|
|
||||||
|
if (anyOrUnknownInArg) {
|
||||||
|
accumulatedAnyOrUnknownArg = accumulatedAnyOrUnknownArg
|
||||||
|
? preserveUnknown(anyOrUnknownInArg, accumulatedAnyOrUnknownArg)
|
||||||
|
: anyOrUnknownInArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.details.paramSpec) {
|
if (type.details.paramSpec) {
|
||||||
@ -10722,7 +10743,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
|
|||||||
return {
|
return {
|
||||||
argumentErrors,
|
argumentErrors,
|
||||||
argResults,
|
argResults,
|
||||||
anyOrUnknownArgument,
|
anyOrUnknownArgument: accumulatedAnyOrUnknownArg,
|
||||||
returnType: specializedReturnType,
|
returnType: specializedReturnType,
|
||||||
isTypeIncomplete,
|
isTypeIncomplete,
|
||||||
activeParam: matchResults.activeParam,
|
activeParam: matchResults.activeParam,
|
||||||
|
@ -2283,6 +2283,40 @@ export function specializeTupleClass(
|
|||||||
return clonedClassType;
|
return clonedClassType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the array contains two or more class types that are all the same
|
||||||
|
// generic type, return the common "erased" type — a type that
|
||||||
|
// replaces all of the type arguments with Unknown.
|
||||||
|
export function getCommonErasedType(types: Type[]): ClassType | undefined {
|
||||||
|
if (types.length < 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClass(types[0])) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < types.length; i++) {
|
||||||
|
const candidate = types[i];
|
||||||
|
if (
|
||||||
|
!isClass(candidate) ||
|
||||||
|
TypeBase.isInstance(candidate) !== TypeBase.isInstance(types[0]) ||
|
||||||
|
!ClassType.isSameGenericClass(types[0], candidate)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTupleClass(types[0])) {
|
||||||
|
return specializeTupleClass(types[0], [{ type: UnknownType.create(), isUnbounded: true }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClassType.cloneForSpecialization(
|
||||||
|
types[0],
|
||||||
|
types[0].details.typeParameters.map((t) => UnknownType.create()),
|
||||||
|
/* isTypeArgumentExplicit */ true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If the type is a function or overloaded function that has a paramSpec
|
// If the type is a function or overloaded function that has a paramSpec
|
||||||
// associated with it and P.args and P.kwargs at the end of the signature,
|
// associated with it and P.args and P.kwargs at the end of the signature,
|
||||||
// it removes these parameters from the function.
|
// it removes these parameters from the function.
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
# This sample tests overload matching in cases where one or more
|
# This sample tests overload matching in cases where one or more
|
||||||
# matches are found due to an Any or Unknown argument.
|
# matches are found due to an Any or Unknown argument.
|
||||||
|
|
||||||
from typing import Any, Literal, overload
|
from __future__ import annotations
|
||||||
|
from typing import Any, Generic, Literal, TypeVar, overload
|
||||||
from typing_extensions import LiteralString
|
from typing_extensions import LiteralString
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def overload1(x: int, y: float) -> float:
|
def overload1(x: int, y: float) -> float:
|
||||||
@ -119,3 +122,82 @@ def unknown_any() -> Any:
|
|||||||
def func5(a: Any):
|
def func5(a: Any):
|
||||||
reveal_type(overload4(a, flag=False), expected_text="str")
|
reveal_type(overload4(a, flag=False), expected_text="str")
|
||||||
reveal_type(overload4("0", flag=a), expected_text="Unknown")
|
reveal_type(overload4("0", flag=a), expected_text="Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload5(x: list[int]) -> list[int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload5(x: list[str]) -> list[str]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def overload5(x: list[str] | list[int]) -> list[str] | list[int]:
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def func6(y: list[Any]):
|
||||||
|
reveal_type(overload5(y), expected_text="list[Unknown]")
|
||||||
|
|
||||||
|
|
||||||
|
class ClassA(Generic[_T]):
|
||||||
|
@overload
|
||||||
|
def m1(self: ClassA[int]) -> ClassA[int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def m1(self: ClassA[str]) -> ClassA[str]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def m1(self) -> ClassA[Any]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def func7(a: ClassA[Any]):
|
||||||
|
reveal_type(a.m1(), expected_text="ClassA[int]")
|
||||||
|
|
||||||
|
|
||||||
|
class ClassB(Generic[_T]):
|
||||||
|
@overload
|
||||||
|
def m1(self: ClassB[int], obj: int | ClassB[int]) -> ClassB[int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def m1(self: ClassB[str], obj: str | ClassB[str]) -> ClassB[str]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def m1(self, obj: Any) -> ClassB[Any]:
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def func8(b: ClassB[Any]):
|
||||||
|
reveal_type(b.m1(b), expected_text="ClassB[Unknown]")
|
||||||
|
|
||||||
|
|
||||||
|
_T1 = TypeVar("_T1")
|
||||||
|
_T2 = TypeVar("_T2")
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload6(a: _T1, /) -> tuple[_T1]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload6(a: _T1, b: _T2, /) -> tuple[_T1, _T2]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload6(*args: _T1) -> tuple[_T1, ...]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def overload6(*args: Any) -> tuple[Any, ...]:
|
||||||
|
return tuple(args)
|
||||||
|
|
||||||
|
|
||||||
|
def func9(*args: int):
|
||||||
|
reveal_type(overload6(*args), expected_text="tuple[int, ...]")
|
||||||
|
Loading…
Reference in New Issue
Block a user