Skip to content

Commit cd079bd

Browse files
authored
[ty] Improve @override, @final and Liskov checks in cases where there are multiple reachable definitions (#21767)
1 parent 5756b38 commit cd079bd

19 files changed

+688
-131
lines changed

crates/ty_ide/src/importer.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,7 @@ impl<'ast> MembersInScope<'ast> {
327327
.members_in_scope_at(node)
328328
.into_iter()
329329
.map(|(name, memberdef)| {
330-
let Some(def) = memberdef.definition else {
331-
return (name, MemberInScope::other(memberdef.ty));
332-
};
330+
let def = memberdef.first_reachable_definition;
333331
let kind = match *def.kind(db) {
334332
DefinitionKind::Import(ref kind) => {
335333
MemberImportKind::Imported(AstImportKind::Import(kind.import(parsed)))
@@ -1891,13 +1889,13 @@ else:
18911889
"#);
18921890
assert_snapshot!(
18931891
test.import_from("foo", "MAGIC"), @r#"
1894-
import foo
1892+
from foo import MAGIC
18951893
if os.getenv("WHATEVER"):
18961894
from foo import MAGIC
18971895
else:
18981896
from bar import MAGIC
18991897
1900-
(foo.MAGIC)
1898+
(MAGIC)
19011899
"#);
19021900
}
19031901

@@ -2108,13 +2106,13 @@ except ImportError:
21082106
");
21092107
assert_snapshot!(
21102108
test.import_from("foo", "MAGIC"), @r"
2111-
import foo
2109+
from foo import MAGIC
21122110
try:
21132111
from foo import MAGIC
21142112
except ImportError:
21152113
from bar import MAGIC
21162114
2117-
(foo.MAGIC)
2115+
(MAGIC)
21182116
");
21192117
}
21202118

crates/ty_python_semantic/resources/mdtest/final.md

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,110 @@ class C(A):
488488
pass
489489

490490
if coinflip():
491-
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
491+
def method2(self) -> None: ... # error: [override-of-final-method]
492492
else:
493-
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
493+
def method2(self) -> None: ...
494494

495495
if coinflip():
496496
def method3(self) -> None: ... # error: [override-of-final-method]
497-
def method4(self) -> None: ... # error: [override-of-final-method]
497+
498+
# TODO: we should emit Liskov violations here too:
499+
if coinflip():
500+
method4 = 42 # error: [override-of-final-method]
501+
else:
502+
method4 = 56
503+
```
504+
505+
## Definitions in statically known branches
506+
507+
```toml
508+
[environment]
509+
python-version = "3.10"
510+
```
511+
512+
```py
513+
import sys
514+
from typing_extensions import final
515+
516+
class Parent:
517+
if sys.version_info >= (3, 10):
518+
@final
519+
def foo(self) -> None: ...
520+
@final
521+
def foooo(self) -> None: ...
522+
@final
523+
def baaaaar(self) -> None: ...
524+
else:
525+
@final
526+
def bar(self) -> None: ...
527+
@final
528+
def baz(self) -> None: ...
529+
@final
530+
def spam(self) -> None: ...
531+
532+
class Child(Parent):
533+
def foo(self) -> None: ... # error: [override-of-final-method]
534+
535+
# The declaration on `Parent` is not reachable,
536+
# so this is fine
537+
def bar(self) -> None: ...
538+
539+
if sys.version_info >= (3, 10):
540+
def foooo(self) -> None: ... # error: [override-of-final-method]
541+
def baz(self) -> None: ...
542+
else:
543+
# Fine because this doesn't override any reachable definitions
544+
def foooo(self) -> None: ...
545+
# There are `@final` definitions being overridden here,
546+
# but the definitions that override them are unreachable
547+
def spam(self) -> None: ...
548+
def baaaaar(self) -> None: ...
549+
```
550+
551+
## Overloads in statically-known branches in stub files
552+
553+
<!-- snapshot-diagnostics -->
554+
555+
```toml
556+
[environment]
557+
python-version = "3.10"
558+
```
559+
560+
```pyi
561+
import sys
562+
from typing_extensions import overload, final
563+
564+
class Foo:
565+
if sys.version_info >= (3, 10):
566+
@overload
567+
@final
568+
def method(self, x: int) -> int: ...
569+
else:
570+
@overload
571+
def method(self, x: int) -> int: ...
572+
@overload
573+
def method(self, x: str) -> str: ...
574+
575+
if sys.version_info >= (3, 10):
576+
@overload
577+
def method2(self, x: int) -> int: ...
578+
else:
579+
@overload
580+
@final
581+
def method2(self, x: int) -> int: ...
582+
@overload
583+
def method2(self, x: str) -> str: ...
584+
585+
class Bar(Foo):
586+
@overload
587+
def method(self, x: int) -> int: ...
588+
@overload
589+
def method(self, x: str) -> str: ... # error: [override-of-final-method]
590+
591+
# This is fine: the only overload that is marked `@final`
592+
# is in a statically-unreachable branch
593+
@overload
594+
def method2(self, x: int) -> int: ...
595+
@overload
596+
def method2(self, x: str) -> str: ...
498597
```

crates/ty_python_semantic/resources/mdtest/liskov.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,3 +583,17 @@ class GoodChild2(Parent):
583583
@staticmethod
584584
def static_method(x: object) -> bool: ...
585585
```
586+
587+
## Definitely bound members with no reachable definitions(!)
588+
589+
We don't emit a Liskov-violation diagnostic here, but if you're writing code like this, you probably
590+
have bigger problems:
591+
592+
```py
593+
from __future__ import annotations
594+
595+
class MaybeEqWhile:
596+
while ...:
597+
def __eq__(self, other: MaybeEqWhile) -> bool:
598+
return True
599+
```

crates/ty_python_semantic/resources/mdtest/named_tuple.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,24 @@ class Child(Base):
610610
# This is fine - Child is not directly a NamedTuple
611611
_asdict = 42
612612
```
613+
614+
## Edge case: multiple reachable definitions with distinct issues
615+
616+
<!-- snapshot-diagnostics -->
617+
618+
```py
619+
from typing import NamedTuple
620+
621+
def coinflip() -> bool:
622+
return True
623+
624+
class Foo(NamedTuple):
625+
if coinflip():
626+
_asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore"
627+
else:
628+
# TODO: there should only be one diagnostic here...
629+
#
630+
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
631+
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
632+
_asdict = True
633+
```

0 commit comments

Comments
 (0)