Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions crates/ty_ide/src/importer.rs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BurntSushi -- both of the tests being changed in this file have FIXME comments above them. But I'm not sure if the changes this PR makes to the tests fixes the problems, or makes the problems worse 😆 I'm not totally sure I understand what these tests are meant to be asserting

Copy link
Member Author

@AlexWaygood AlexWaygood Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LMK if you consider this a regression and I can fix it in a followup. It shouldn't be too hard to track whether a member has multiple definitions and propagate that information upwards, if that's what the autocomplete machinery wants to know

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea here was just trying to test what happens when we try to insert an import for a symbol that is already imported, but whose imports are conditional. I think in this case we don't want to add any new imports. But we weren't doing that before either. So I think this is fine for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, thank you!

Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,7 @@ impl<'ast> MembersInScope<'ast> {
.members_in_scope_at(node)
.into_iter()
.map(|(name, memberdef)| {
let Some(def) = memberdef.definition else {
return (name, MemberInScope::other(memberdef.ty));
};
let def = memberdef.first_reachable_definition;
let kind = match *def.kind(db) {
DefinitionKind::Import(ref kind) => {
MemberImportKind::Imported(AstImportKind::Import(kind.import(parsed)))
Expand Down Expand Up @@ -1891,13 +1889,13 @@ else:
"#);
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r#"
import foo
from foo import MAGIC
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC

(foo.MAGIC)
(MAGIC)
"#);
}

Expand Down Expand Up @@ -2108,13 +2106,13 @@ except ImportError:
");
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r"
import foo
from foo import MAGIC
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC

(foo.MAGIC)
(MAGIC)
");
}

Expand Down
105 changes: 102 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/final.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,110 @@ class C(A):
pass

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

if coinflip():
def method3(self) -> None: ... # error: [override-of-final-method]
def method4(self) -> None: ... # error: [override-of-final-method]

# TODO: we should emit Liskov violations here too:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we do? At least at the first definition...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, we emit a diagnostic stating that an @final method has been overridden (which is not to my mind a Liskov violation), but we are silent about the fact that the type has also been incompatibly overridden (which is, to my mind, a Liskov violation)

if coinflip():
method4 = 42 # error: [override-of-final-method]
else:
method4 = 56
```

## Definitions in statically known branches

```toml
[environment]
python-version = "3.10"
```

```py
import sys
from typing_extensions import final

class Parent:
if sys.version_info >= (3, 10):
@final
def foo(self) -> None: ...
@final
def foooo(self) -> None: ...
@final
def baaaaar(self) -> None: ...
else:
@final
def bar(self) -> None: ...
@final
def baz(self) -> None: ...
@final
def spam(self) -> None: ...

class Child(Parent):
def foo(self) -> None: ... # error: [override-of-final-method]

# The declaration on `Parent` is not reachable,
# so this is fine
def bar(self) -> None: ...

if sys.version_info >= (3, 10):
def foooo(self) -> None: ... # error: [override-of-final-method]
def baz(self) -> None: ...
else:
# Fine because this doesn't override any reachable definitions
def foooo(self) -> None: ...
# There are `@final` definitions being overridden here,
# but the definitions that override them are unreachable
def spam(self) -> None: ...
def baaaaar(self) -> None: ...
```

## Overloads in statically-known branches in stub files

<!-- snapshot-diagnostics -->

```toml
[environment]
python-version = "3.10"
```

```pyi
import sys
from typing_extensions import overload, final

class Foo:
if sys.version_info >= (3, 10):
@overload
@final
def method(self, x: int) -> int: ...
else:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...

if sys.version_info >= (3, 10):
@overload
def method2(self, x: int) -> int: ...
else:
@overload
@final
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...

class Bar(Foo):
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ... # error: [override-of-final-method]

# This is fine: the only overload that is marked `@final`
# is in a statically-unreachable branch
@overload
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...
```
14 changes: 14 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/liskov.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,17 @@ class GoodChild2(Parent):
@staticmethod
def static_method(x: object) -> bool: ...
```

## Definitely bound members with no reachable definitions(!)

We don't emit a Liskov-violation diagnostic here, but if you're writing code like this, you probably
have bigger problems:

```py
from __future__ import annotations

class MaybeEqWhile:
while ...:
def __eq__(self, other: MaybeEqWhile) -> bool:
return True
```
21 changes: 21 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,24 @@ class Child(Base):
# This is fine - Child is not directly a NamedTuple
_asdict = 42
```

## Edge case: multiple reachable definitions with distinct issues

<!-- snapshot-diagnostics -->

```py
from typing import NamedTuple

def coinflip() -> bool:
return True

class Foo(NamedTuple):
if coinflip():
_asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore"
else:
# TODO: there should only be one diagnostic here...
#
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
_asdict = True
```
Loading
Loading