[ty] Emit diagnostic for unimplemented abstract method on @final class#22753
[ty] Emit diagnostic for unimplemented abstract method on @final class#22753charliermarsh merged 12 commits intomainfrom
Conversation
Typing conformance resultsNo changes detected ✅ |
aa9e238 to
3cb5b97
Compare
|
3cb5b97 to
b518c4d
Compare
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-await |
2 | 0 | 6 |
invalid-parameter-default |
0 | 0 | 7 |
invalid-return-type |
0 | 5 | 2 |
possibly-missing-attribute |
3 | 0 | 1 |
invalid-argument-type |
1 | 0 | 1 |
unresolved-attribute |
0 | 0 | 2 |
unused-ignore-comment |
0 | 1 | 0 |
| Total | 6 | 6 | 19 |
...ests_for_the_`@typi…_-_A_`@final`_class_mus…_-_Multiple_abstract_me…_(feafee9a4abbe8d1).snap
Outdated
Show resolved
Hide resolved
b930909 to
8e17e63
Compare
8e17e63 to
248ab2a
Compare
|
now you're emitting false-positive errors on classes like this, which can be instantiated at runtime 😄 from abc import abstractmethod, ABC
from typing import final
class Base(ABC):
@property
@abstractmethod
def f(self) -> int: ...
@final
class Child(Base):
f = 42The difference between @final
class BadChild(Base):
f: intis that |
|
Oh gosh. |
9273cf0 to
6e68821
Compare
| .with_qualifiers(type_and_qualifiers.qualifiers()), | ||
| Ok(type_and_qualifiers) => Place::Defined( | ||
| DefinedPlace::new(type_and_qualifiers.inner_type()) | ||
| .with_origin(type_and_qualifiers.origin()), |
There was a problem hiding this comment.
I think we were losing the origin here (unintentionally?).
There was a problem hiding this comment.
yes, this looks correct! good catch
6e68821 to
8adff03
Compare
| class Derived(Base): # No error - not final, can be subclassed | ||
| pass |
There was a problem hiding this comment.
(but we should emit an error when such a class is instantiated, of course -- which is astral-sh/ty#1877; it doesn't need to be implemented here)
| /// Only returns methods that are still abstract on `self` (i.e., have not been overridden | ||
| /// with a concrete implementation anywhere in the MRO). | ||
| #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] | ||
| pub(crate) fn abstract_methods(self, db: &'db dyn Db) -> FxHashMap<Name, ClassType<'db>> { |
There was a problem hiding this comment.
I think there are probably marginally more efficient ways of doing this with a single MRO iteration (if we iterated through the MRO in reverse order), but this also seems fine for a first pass
| for (method_name, defining_class) in class_type.abstract_methods(db) { | ||
| let Some(builder) = self | ||
| .context | ||
| .report_lint(&UNIMPLEMENTED_ABSTRACT_METHOD, &class_node.name) | ||
| else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
if there are multiple unimplemented abstract methods, do we really want to emit a separate diagnostic for each one? Wouldn't it be better to collect a list of all the unimplemented abstract methods and then emit a single diagnostic saying "Methods foo, bar, baz are all abstract"? (But I think it would be fine to only add a subdiagnostic pointing to the definition of the first one; too many subdiagnostics would be noisy.)
There was a problem hiding this comment.
I personally find this more useful + consistent with our other patterns, but I don't feel strongly.
There was a problem hiding this comment.
I know there's still a bunch of rules where we emit a separate diagnostic for each element in cases like this, but in general I think we aspire to group them together where possible -- e.g. here we only emit a single diagnostic, even though multiple elements of the union have the incorrect signature:
% uvx ty check foo.py
error[unknown-argument]: Argument `x` does not match any known parameter of function `f`
--> foo.py:7:10
|
5 | def i(coinflip: bool):
6 | func = f if coinflip else g if coinflip else h
7 | func(x=42)
| ^^^^
|
info: Union variant `def f() -> Unknown` is incompatible with this call site
info: Attempted to call union type `(def f() -> Unknown) | (def h(x) -> Unknown)`
info: rule `unknown-argument` is enabled by default
Found 1 diagnostic
% cat foo.py
def f(): ...
def g(): ...
def h(x): ...
def i(coinflip: bool):
func = f if coinflip else g if coinflip else h
func(x=42)
d6ab7ac to
c2a575c
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
This looks great, thank you
| ### Protocol with abstract methods | ||
|
|
||
| ```py | ||
| from abc import abstractmethod | ||
| from typing import Protocol, final | ||
|
|
||
| class MyProtocol(Protocol): | ||
| @abstractmethod | ||
| def method(self) -> int: ... | ||
|
|
||
| @final | ||
| class Implementer(MyProtocol): # error: [unimplemented-abstract-method] | ||
| pass | ||
| ``` |
There was a problem hiding this comment.
(this can be a followup): we also need to implement the rule where all methods on Protocol classes that have "trivial bodies" (either ..., pass, a docstring, or a combination of those three elements) and have an explicit non-None return type are treated as implicitly abstract even if they are not decorated with @abstractmethod
c2a575c to
977155e
Compare
977155e to
edb828c
Compare
Summary
Closes astral-sh/ty#2525.