mirror of https://github.com/astral-sh/ruff
[red-knot] Check overloads without an implementation (#17681)
## Summary As mentioned in the spec (https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions), part of #15383: > The `@overload`-decorated definitions must be followed by an overload implementation, which does not include an `@overload` decorator. Type checkers should report an error or warning if an implementation is missing. Overload definitions within stub files, protocols, and on abstract methods within abstract base classes are exempt from this check. ## Test Plan Remove TODOs from the test; create one diagnostic snapshot.
This commit is contained in:
parent
f584b66824
commit
7825975972
|
|
@ -205,7 +205,7 @@ reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or
|
||||||
|
|
||||||
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
|
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
|
||||||
|
|
||||||
```py
|
```pyi
|
||||||
from types import FunctionType, MethodType
|
from types import FunctionType, MethodType
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,23 +336,25 @@ def func(x: int) -> int: ...
|
||||||
|
|
||||||
#### Regular modules
|
#### Regular modules
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one
|
In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one
|
||||||
non-`@overload`-decorated definition (for the same function/method).
|
non-`@overload`-decorated definition (for the same function/method).
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
|
||||||
# TODO: error because implementation does not exists
|
|
||||||
@overload
|
@overload
|
||||||
def func(x: int) -> int: ...
|
def func(x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
|
# error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation"
|
||||||
def func(x: str) -> str: ...
|
def func(x: str) -> str: ...
|
||||||
|
|
||||||
class Foo:
|
class Foo:
|
||||||
# TODO: error because implementation does not exists
|
|
||||||
@overload
|
@overload
|
||||||
def method(self, x: int) -> int: ...
|
def method(self, x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
|
# error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation"
|
||||||
def method(self, x: str) -> str: ...
|
def method(self, x: str) -> str: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -405,12 +407,12 @@ from it.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Foo:
|
class Foo:
|
||||||
# TODO: Error because implementation does not exists
|
|
||||||
@overload
|
@overload
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def f(self, x: int) -> int: ...
|
def f(self, x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
# error: [invalid-overload]
|
||||||
def f(self, x: str) -> str: ...
|
def f(self, x: str) -> str: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -422,6 +424,7 @@ class PartialFoo1(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def f(self, x: int) -> int: ...
|
def f(self, x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
|
# error: [invalid-overload]
|
||||||
def f(self, x: str) -> str: ...
|
def f(self, x: str) -> str: ...
|
||||||
|
|
||||||
class PartialFoo(ABC):
|
class PartialFoo(ABC):
|
||||||
|
|
@ -429,6 +432,7 @@ class PartialFoo(ABC):
|
||||||
def f(self, x: int) -> int: ...
|
def f(self, x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
# error: [invalid-overload]
|
||||||
def f(self, x: str) -> str: ...
|
def f(self, x: str) -> str: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: overloads.md - Overloads - Invalid - Overload without an implementation - Regular modules
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | from typing import overload
|
||||||
|
2 |
|
||||||
|
3 | @overload
|
||||||
|
4 | def func(x: int) -> int: ...
|
||||||
|
5 | @overload
|
||||||
|
6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation"
|
||||||
|
7 | def func(x: str) -> str: ...
|
||||||
|
8 |
|
||||||
|
9 | class Foo:
|
||||||
|
10 | @overload
|
||||||
|
11 | def method(self, x: int) -> int: ...
|
||||||
|
12 | @overload
|
||||||
|
13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation"
|
||||||
|
14 | def method(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-overload: Overloaded non-stub function `func` must have an implementation
|
||||||
|
--> src/mdtest_snippet.py:7:5
|
||||||
|
|
|
||||||
|
5 | @overload
|
||||||
|
6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation"
|
||||||
|
7 | def func(x: str) -> str: ...
|
||||||
|
| ^^^^
|
||||||
|
8 |
|
||||||
|
9 | class Foo:
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:invalid-overload: Overloaded non-stub function `method` must have an implementation
|
||||||
|
--> src/mdtest_snippet.py:14:9
|
||||||
|
|
|
||||||
|
12 | @overload
|
||||||
|
13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation"
|
||||||
|
14 | def method(self, x: str) -> str: ...
|
||||||
|
| ^^^^^^
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -562,6 +562,13 @@ impl<'db> ClassLiteral<'db> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if this is an abstract class.
|
||||||
|
pub(super) fn is_abstract(self, db: &'db dyn Db) -> bool {
|
||||||
|
self.metaclass(db)
|
||||||
|
.into_class_literal()
|
||||||
|
.is_some_and(|metaclass| metaclass.is_known(db, KnownClass::ABCMeta))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the types of the decorators on this class
|
/// Return the types of the decorators on this class
|
||||||
#[salsa::tracked(return_ref)]
|
#[salsa::tracked(return_ref)]
|
||||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||||
|
|
|
||||||
|
|
@ -744,7 +744,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
// TODO: Only call this function when diagnostics are enabled.
|
// TODO: Only call this function when diagnostics are enabled.
|
||||||
self.check_class_definitions();
|
self.check_class_definitions();
|
||||||
self.check_overloaded_functions();
|
self.check_overloaded_functions(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all class definitions to check that the definition will not cause an exception
|
/// Iterate over all class definitions to check that the definition will not cause an exception
|
||||||
|
|
@ -987,7 +987,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
///
|
///
|
||||||
/// For (1), this has the consequence of not checking an overloaded function that is being
|
/// For (1), this has the consequence of not checking an overloaded function that is being
|
||||||
/// shadowed by another function with the same name in this scope.
|
/// shadowed by another function with the same name in this scope.
|
||||||
fn check_overloaded_functions(&mut self) {
|
fn check_overloaded_functions(&mut self, scope: &NodeWithScopeKind) {
|
||||||
// Collect all the unique overloaded function symbols in this scope. This requires a set
|
// Collect all the unique overloaded function symbols in this scope. This requires a set
|
||||||
// because an overloaded function uses the same symbol for each of the overloads and the
|
// because an overloaded function uses the same symbol for each of the overloads and the
|
||||||
// implementation.
|
// implementation.
|
||||||
|
|
@ -1056,6 +1056,46 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that the overloaded function has an implementation. Overload definitions
|
||||||
|
// within stub files, protocols, and on abstract methods within abstract base classes
|
||||||
|
// are exempt from this check.
|
||||||
|
if overloaded.implementation.is_none() && !self.in_stub() {
|
||||||
|
let mut implementation_required = true;
|
||||||
|
|
||||||
|
if let NodeWithScopeKind::Class(class_node_ref) = scope {
|
||||||
|
let class = binding_type(
|
||||||
|
self.db(),
|
||||||
|
self.index.expect_single_definition(class_node_ref.node()),
|
||||||
|
)
|
||||||
|
.expect_class_literal();
|
||||||
|
|
||||||
|
if class.is_protocol(self.db())
|
||||||
|
|| (class.is_abstract(self.db())
|
||||||
|
&& overloaded.overloads.iter().all(|overload| {
|
||||||
|
overload.has_known_decorator(
|
||||||
|
self.db(),
|
||||||
|
FunctionDecorators::ABSTRACT_METHOD,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
implementation_required = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if implementation_required {
|
||||||
|
let function_node = function.node(self.db(), self.file());
|
||||||
|
if let Some(builder) = self
|
||||||
|
.context
|
||||||
|
.report_lint(&INVALID_OVERLOAD, &function_node.name)
|
||||||
|
{
|
||||||
|
builder.into_diagnostic(format_args!(
|
||||||
|
"Overloaded non-stub function `{}` must have an implementation",
|
||||||
|
&function_node.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue