From 78259759726f30b9aecff165f15ac449e8e4f915 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 19:54:21 +0530 Subject: [PATCH] [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. --- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/overloads.md | 10 +++- ...t_an_implementation_-_Regular_modules.snap | 57 +++++++++++++++++++ .../src/types/class.rs | 7 +++ .../src/types/infer.rs | 44 +++++++++++++- 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 9ca10a05c7..3abc301372 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -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: -```py +```pyi from types import FunctionType, MethodType from typing import overload diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/red_knot_python_semantic/resources/mdtest/overloads.md index 4bbef31b1d..4089040d9b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/overloads.md +++ b/crates/red_knot_python_semantic/resources/mdtest/overloads.md @@ -336,23 +336,25 @@ def func(x: int) -> int: ... #### Regular modules + + In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one non-`@overload`-decorated definition (for the same function/method). ```py from typing import overload -# TODO: error because implementation does not exists @overload def func(x: int) -> int: ... @overload +# error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" def func(x: str) -> str: ... class Foo: - # TODO: error because implementation does not exists @overload def method(self, x: int) -> int: ... @overload + # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" def method(self, x: str) -> str: ... ``` @@ -405,12 +407,12 @@ from it. ```py class Foo: - # TODO: Error because implementation does not exists @overload @abstractmethod def f(self, x: int) -> int: ... @overload @abstractmethod + # error: [invalid-overload] def f(self, x: str) -> str: ... ``` @@ -422,6 +424,7 @@ class PartialFoo1(ABC): @abstractmethod def f(self, x: int) -> int: ... @overload + # error: [invalid-overload] def f(self, x: str) -> str: ... class PartialFoo(ABC): @@ -429,6 +432,7 @@ class PartialFoo(ABC): def f(self, x: int) -> int: ... @overload @abstractmethod + # error: [invalid-overload] def f(self, x: str) -> str: ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap new file mode 100644 index 0000000000..f92141e10c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap @@ -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: ... + | ^^^^^^ + | + +``` diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index a2ccc66e8d..b68e6b9798 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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 #[salsa::tracked(return_ref)] fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d9a15f0cc6..b9c56a02af 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -744,7 +744,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Only call this function when diagnostics are enabled. 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 @@ -987,7 +987,7 @@ impl<'db> TypeInferenceBuilder<'db> { /// /// 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. - 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 // because an overloaded function uses the same symbol for each of the overloads and the // 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 + )); + } + } + } } }