mirror of https://github.com/astral-sh/ruff
[red-knot] Fix callable subtyping for standard parameters (#17125)
## Summary This PR fixes a bug in callable subtyping to consider both the positional and keyword form of the standard parameter in the supertype when matching against variadic, keyword-only and keyword-variadic parameter in the subtype. This is done by collecting the unmatched standard parameters and then checking them against the keyword-only / keyword-variadic parameters after the positional loop. ## Test Plan Add test cases.
This commit is contained in:
parent
c74ba00219
commit
d29d4956de
|
|
@ -858,6 +858,52 @@ static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword
|
||||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic]))
|
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
But, there are special cases when matching against standard parameters. This is due to the fact that
|
||||||
|
a standard parameter can be passed as a positional or keyword parameter. This means that the
|
||||||
|
subtyping relation needs to consider both cases.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def variadic_keyword(*args: int, **kwargs: int) -> None: ...
|
||||||
|
def standard_int(a: int) -> None: ...
|
||||||
|
def standard_float(a: float) -> None: ...
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float]))
|
||||||
|
```
|
||||||
|
|
||||||
|
If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard
|
||||||
|
parameter, then the subtyping relation is invalid.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def variadic_bool(*args: bool, **kwargs: int) -> None: ...
|
||||||
|
def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ...
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int]))
|
||||||
|
```
|
||||||
|
|
||||||
|
The standard parameter can follow a variadic parameter in the subtype.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def standard_variadic_int(a: int, *args: int) -> None: ...
|
||||||
|
def standard_variadic_float(a: int, *args: float) -> None: ...
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float]))
|
||||||
|
```
|
||||||
|
|
||||||
|
The keyword part of the standard parameter can be matched against keyword-only parameter with the
|
||||||
|
same name if the keyword-variadic parameter is absent.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def variadic_a(*args: int, a: int) -> None: ...
|
||||||
|
def variadic_b(*args: int, b: int) -> None: ...
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int]))
|
||||||
|
# The parameter name is different
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int]))
|
||||||
|
```
|
||||||
|
|
||||||
#### Keyword-only
|
#### Keyword-only
|
||||||
|
|
||||||
For keyword-only parameters, the name should be the same:
|
For keyword-only parameters, the name should be the same:
|
||||||
|
|
|
||||||
|
|
@ -4644,6 +4644,10 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
iter_other: other_signature.parameters().iter(),
|
iter_other: other_signature.parameters().iter(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Collect all the standard parameters that have only been matched against a variadic
|
||||||
|
// parameter which means that the keyword variant is still unmatched.
|
||||||
|
let mut other_keywords = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Some(next_parameter) = parameters.next() else {
|
let Some(next_parameter) = parameters.next() else {
|
||||||
// All parameters have been checked or both the parameter lists were empty. In
|
// All parameters have been checked or both the parameter lists were empty. In
|
||||||
|
|
@ -4653,6 +4657,14 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
|
|
||||||
match next_parameter {
|
match next_parameter {
|
||||||
EitherOrBoth::Left(self_parameter) => match self_parameter.kind() {
|
EitherOrBoth::Left(self_parameter) => match self_parameter.kind() {
|
||||||
|
ParameterKind::KeywordOnly { .. } | ParameterKind::KeywordVariadic { .. }
|
||||||
|
if !other_keywords.is_empty() =>
|
||||||
|
{
|
||||||
|
// If there are any unmatched keyword parameters in `other`, they need to
|
||||||
|
// be checked against the keyword-only / keyword-variadic parameters that
|
||||||
|
// will be done after this loop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
ParameterKind::PositionalOnly { default_type, .. }
|
ParameterKind::PositionalOnly { default_type, .. }
|
||||||
| ParameterKind::PositionalOrKeyword { default_type, .. }
|
| ParameterKind::PositionalOrKeyword { default_type, .. }
|
||||||
| ParameterKind::KeywordOnly { default_type, .. } => {
|
| ParameterKind::KeywordOnly { default_type, .. } => {
|
||||||
|
|
@ -4727,7 +4739,11 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(ParameterKind::Variadic { .. }, ParameterKind::PositionalOnly { .. }) => {
|
(
|
||||||
|
ParameterKind::Variadic { .. },
|
||||||
|
ParameterKind::PositionalOnly { .. }
|
||||||
|
| ParameterKind::PositionalOrKeyword { .. },
|
||||||
|
) => {
|
||||||
if !check_types(
|
if !check_types(
|
||||||
other_parameter.annotated_type(),
|
other_parameter.annotated_type(),
|
||||||
self_parameter.annotated_type(),
|
self_parameter.annotated_type(),
|
||||||
|
|
@ -4735,6 +4751,13 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
other_parameter.kind(),
|
||||||
|
ParameterKind::PositionalOrKeyword { .. }
|
||||||
|
) {
|
||||||
|
other_keywords.push(other_parameter);
|
||||||
|
}
|
||||||
|
|
||||||
// We've reached a variadic parameter in `self` which means there can
|
// We've reached a variadic parameter in `self` which means there can
|
||||||
// be no more positional parameters after this in a valid AST. But, the
|
// be no more positional parameters after this in a valid AST. But, the
|
||||||
// current parameter in `other` is a positional-only which means there
|
// current parameter in `other` is a positional-only which means there
|
||||||
|
|
@ -4749,15 +4772,18 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
let Some(other_parameter) = parameters.peek_other() else {
|
let Some(other_parameter) = parameters.peek_other() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
if !matches!(
|
match other_parameter.kind() {
|
||||||
other_parameter.kind(),
|
ParameterKind::PositionalOrKeyword { .. } => {
|
||||||
|
other_keywords.push(other_parameter);
|
||||||
|
}
|
||||||
ParameterKind::PositionalOnly { .. }
|
ParameterKind::PositionalOnly { .. }
|
||||||
| ParameterKind::Variadic { .. }
|
| ParameterKind::Variadic { .. } => {}
|
||||||
) {
|
_ => {
|
||||||
// Any other parameter kind cannot be checked against a
|
// Any other parameter kind cannot be checked against a
|
||||||
// variadic parameter and is deferred to the next iteration.
|
// variadic parameter and is deferred to the next iteration.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if !check_types(
|
if !check_types(
|
||||||
other_parameter.annotated_type(),
|
other_parameter.annotated_type(),
|
||||||
self_parameter.annotated_type(),
|
self_parameter.annotated_type(),
|
||||||
|
|
@ -4828,11 +4854,15 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for other_parameter in other_parameters {
|
for other_parameter in other_keywords.into_iter().chain(other_parameters) {
|
||||||
match other_parameter.kind() {
|
match other_parameter.kind() {
|
||||||
ParameterKind::KeywordOnly {
|
ParameterKind::KeywordOnly {
|
||||||
name: other_name,
|
name: other_name,
|
||||||
default_type: other_default,
|
default_type: other_default,
|
||||||
|
}
|
||||||
|
| ParameterKind::PositionalOrKeyword {
|
||||||
|
name: other_name,
|
||||||
|
default_type: other_default,
|
||||||
} => {
|
} => {
|
||||||
if let Some(self_parameter) = self_keywords.remove(other_name) {
|
if let Some(self_parameter) = self_keywords.remove(other_name) {
|
||||||
match self_parameter.kind() {
|
match self_parameter.kind() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue