mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Offer completions for T when a value has type Unknown | T
Fixes astral-sh/ty#2197
This commit is contained in:
committed by
Andrew Gallant
parent
4cba2e8f91
commit
952193e0c6
@@ -7741,6 +7741,68 @@ TypedDi<CURSOR>
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that `xs = ["..."]; xs[0].<CURSOR>` gets completions
|
||||
/// appropriate for `str`.
|
||||
#[test]
|
||||
fn dynamic_type_list_no_type_annotation() {
|
||||
let builder = completion_test_builder(
|
||||
r#"
|
||||
my_list = ["foo"]
|
||||
my_list[0].remove<CURSOR>
|
||||
"#,
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that when we have `Any | T` that we offer
|
||||
/// completions for `T`.
|
||||
#[test]
|
||||
fn dynamic_type_with_type_annotation() {
|
||||
let builder = completion_test_builder(
|
||||
r#"
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any | str):
|
||||
x.remove<CURSOR>
|
||||
"#,
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that when we have `(U & Any) | T` that we offer
|
||||
/// completions for `T`.
|
||||
#[test]
|
||||
fn dynamic_type_with_intersection_type_annotation() {
|
||||
let builder = completion_test_builder(
|
||||
r#"
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection
|
||||
|
||||
def f(x: Intersection[int, Any] | str):
|
||||
x.remove<CURSOR>
|
||||
"#,
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
|
||||
@@ -361,6 +361,52 @@ def f(union: A | B):
|
||||
static_assert(not has_member(union, "only_on_b"))
|
||||
```
|
||||
|
||||
Unless one of the elements of the union is `Any`, thus making it dynamic. In which case, we consider
|
||||
items on the intersection of the non-`Any` elements:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(union: Any | A):
|
||||
static_assert(has_member(union, "on_both"))
|
||||
static_assert(has_member(union, "only_on_a"))
|
||||
|
||||
def g(union: Any | A | B):
|
||||
static_assert(has_member(union, "on_both"))
|
||||
static_assert(not has_member(union, "only_on_a"))
|
||||
static_assert(not has_member(union, "only_on_b"))
|
||||
```
|
||||
|
||||
Similarly, unioning with an intersection involving `Any` is treated the same as if it was just
|
||||
unioned with `Any`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, has_member, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(x: Intersection[Any, A] | B):
|
||||
static_assert(has_member(x, "on_both"))
|
||||
static_assert(not has_member(x, "only_on_a"))
|
||||
static_assert(has_member(x, "only_on_b"))
|
||||
```
|
||||
|
||||
### Intersections
|
||||
|
||||
#### Only positive types
|
||||
|
||||
@@ -1064,7 +1064,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_dynamic(&self) -> bool {
|
||||
pub(crate) const fn is_dynamic(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
|
||||
@@ -163,14 +163,33 @@ impl<'db> AllMembers<'db> {
|
||||
|
||||
fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
match ty {
|
||||
Type::Union(union) => self.members.extend(
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| AllMembers::of(db, *ty).members)
|
||||
.reduce(|acc, members| acc.intersection(&members).cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
Type::Union(union) => {
|
||||
fn is_dynamic(db: &dyn Db, ty: Type<'_>) -> bool {
|
||||
// We don't need to use recursion here because
|
||||
// `Type` guarantees that unions/intersections
|
||||
// are kept in DNF (i.e., they are flattened).
|
||||
ty.is_dynamic()
|
||||
|| match ty {
|
||||
Type::Intersection(intersection) => {
|
||||
intersection.positive(db).iter().any(Type::is_dynamic)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
let union = match union.filter(db, |&ty| !is_dynamic(db, ty)) {
|
||||
Type::Union(union) => union,
|
||||
ty => return self.extend_with_type(db, ty),
|
||||
};
|
||||
self.members.extend(
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| AllMembers::of(db, *ty).members)
|
||||
.reduce(|acc, members| acc.intersection(&members).cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
Type::Intersection(intersection) => self.members.extend(
|
||||
intersection
|
||||
|
||||
Reference in New Issue
Block a user