Fixed a bug that results in incorrect type compatibility checks for a… (#7994)

* Fixed a bug that results in incorrect type compatibility checks for a callable that uses `*args: *tuple[T, *tuple[S, ...]]`. This addresses #7987.

* Improved diagnostic messages for parameter mismatch.
This commit is contained in:
Eric Traut 2024-05-24 13:19:16 -07:00 committed by GitHub
parent af916c6644
commit 0a83d6459c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 70 deletions

View File

@ -25298,76 +25298,83 @@ export function createTypeEvaluator(
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) {
// All source parameters that don't have a default value must
// have a matching parameter in the dest.
if (destPositionalCount < srcPositionalCount && !targetIncludesParamSpec) {
for (let i = destPositionalCount; i < srcPositionalCount; i++) {
// If the dest has an *args parameter, make sure it can accept the remaining
// positional arguments in the source.
if (destParamDetails.argsIndex !== undefined) {
const destArgsType = destParamDetails.params[destParamDetails.argsIndex].type;
const srcParamType = srcParamDetails.params[i].type;
if (
!assignFunctionParameter(
destArgsType,
srcParamType,
i,
diag?.createAddendum(),
destTypeVarContext,
srcTypeVarContext,
flags,
recursionCount
)
) {
canAssign = false;
}
continue;
}
// If The source parameter has a default value, it is OK for the
// corresponding dest parameter to be missing.
const srcParam = srcParamDetails.params[i];
if (srcParam.param.hasDefault) {
// Assign default arg value in case it is needed for
// populating TypeVar constraints.
const paramInfo = srcParamDetails.params[i];
const defaultArgType = paramInfo.defaultArgType ?? paramInfo.param.defaultType;
if (
defaultArgType &&
!assignType(
paramInfo.type,
defaultArgType,
diag?.createAddendum(),
srcTypeVarContext,
/* destTypeVarContext */ undefined,
flags,
recursionCount
)
) {
canAssign = false;
}
continue;
}
// If the source parameter is also addressible by keyword, it is OK
// that there is no matching positional parameter in the dest.
if (srcParam.kind === ParameterKind.Standard) {
continue;
}
// If the source parameter is a variadic, it is OK that there is no
// matching positional parameter in the dest.
if (srcParam.param.category === ParameterCategory.ArgsList) {
continue;
}
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) {
const destPositionOnlyCount = destParamDetails.firstPositionOrKeywordIndex;
if (destPositionOnlyCount > 0 && destPositionOnlyCount < srcPositionalCount) {
diag?.createAddendum().addMessage(
LocAddendum.functionTooFewParams().format({
expected: nonDefaultSrcParamCount,
received: destPositionalCount,
})
);
canAssign = false;
}
} else {
// Assign default arg values in case they are needed for
// populating TypeVar constraints.
for (let i = destParamDetails.firstPositionOrKeywordIndex; i < srcPositionalCount; i++) {
const paramInfo = srcParamDetails.params[i];
const defaultArgType = paramInfo.defaultArgType ?? paramInfo.param.defaultType;
if (
defaultArgType &&
!assignType(
paramInfo.type,
defaultArgType,
diag?.createAddendum(),
srcTypeVarContext,
/* destTypeVarContext */ undefined,
flags,
recursionCount
)
) {
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 (
!assignFunctionParameter(
destArgsType,
srcParamType,
paramIndex,
diag?.createAddendum(),
destTypeVarContext,
srcTypeVarContext,
flags,
recursionCount
)
) {
canAssign = false;
}
}
}
}
diag?.createAddendum().addMessage(
LocAddendum.functionTooFewParams().format({
expected: nonDefaultSrcParamCount,
received: destPositionalCount,
})
);
canAssign = false;
break;
}
} else if (srcPositionalCount < destPositionalCount) {
if (srcParamDetails.argsIndex !== undefined) {
@ -25522,7 +25529,7 @@ export function createTypeEvaluator(
if (!destParamInfo) {
if (destParamDetails.kwargsIndex === undefined && !srcParamInfo.param.hasDefault) {
if (paramDiag && srcParamDetails.firstKeywordOnlyIndex !== undefined) {
if (paramDiag) {
paramDiag.addMessage(
LocAddendum.namedParamMissingInDest().format({
name: srcParamInfo.param.name,

View File

@ -679,8 +679,8 @@
"missingGetter": "Property getter method is missing",
"missingSetter": "Property setter method is missing",
"missingDeleter": "Property deleter method is missing",
"namedParamMissingInDest": "Keyword parameter \"{name}\" is missing in destination",
"namedParamMissingInSource": "Keyword parameter \"{name}\" is missing in source",
"namedParamMissingInDest": "Extra parameter \"{name}\"",
"namedParamMissingInSource": "Missing keyword parameter \"{name}\"",
"namedParamTypeMismatch": "Keyword parameter \"{name}\" of type \"{sourceType}\" is incompatible with type \"{destType}\"",
"namedTupleNotAllowed": "NamedTuple cannot be used for instance or class checks",
"newMethodLocation": "The __new__ method is defined in class \"{type}\"",

View File

@ -122,3 +122,27 @@ def test_func5(a: int, b: str) -> int:
f5: TestClass5 = test_func5
class TestClass6(Protocol):
def __call__(self, a: int, /, *, b: str) -> int: ...
def test_func6(a: int, b: str) -> int:
return 123
f6: TestClass6 = test_func6
class TestClass7:
def __call__(self) -> None:
pass
def test_func7(*args: * tuple[int, *tuple[int, ...]]) -> int:
return 123
# This should generate an error.
f7: TestClass7 = test_func7

View File

@ -16,7 +16,7 @@ import * as TestUtils from './testUtils';
test('CallbackProtocol1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['callbackProtocol1.py']);
TestUtils.validateResults(analysisResults, 9);
TestUtils.validateResults(analysisResults, 10);
});
test('CallbackProtocol2', () => {