[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:
Dhruv Manilawala 2025-04-30 19:54:21 +05:30 committed by GitHub
parent f584b66824
commit 7825975972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 114 additions and 6 deletions

View File

@ -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

View File

@ -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: ...
``` ```

View File

@ -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: ...
| ^^^^^^
|
```

View File

@ -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>]> {

View File

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