Skip to content

Commit 6e7ff07

Browse files
authored
[ty] Provide completions on TypeVars (#20943)
## Summary closes astral-sh/ty#1370 ## Test Plan New snapshot tests
1 parent c7e2bfd commit 6e7ff07

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed

crates/ty_ide/src/completion.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3896,6 +3896,55 @@ print(t'''{Foo} and Foo.zqzq<CURSOR>
38963896
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
38973897
}
38983898

3899+
#[test]
3900+
fn typevar_with_upper_bound() {
3901+
let test = cursor_test(
3902+
"\
3903+
def f[T: str](msg: T):
3904+
msg.<CURSOR>
3905+
",
3906+
);
3907+
test.assert_completions_include("upper");
3908+
test.assert_completions_include("capitalize");
3909+
}
3910+
3911+
#[test]
3912+
fn typevar_with_constraints() {
3913+
// Test TypeVar with constraints
3914+
let test = cursor_test(
3915+
"\
3916+
from typing import TypeVar
3917+
3918+
class A:
3919+
only_on_a: int
3920+
on_a_and_b: str
3921+
3922+
class B:
3923+
only_on_b: float
3924+
on_a_and_b: str
3925+
3926+
T = TypeVar('T', A, B)
3927+
3928+
def f(x: T):
3929+
x.<CURSOR>
3930+
",
3931+
);
3932+
test.assert_completions_include("on_a_and_b");
3933+
test.assert_completions_do_not_include("only_on_a");
3934+
test.assert_completions_do_not_include("only_on_b");
3935+
}
3936+
3937+
#[test]
3938+
fn typevar_without_bounds_or_constraints() {
3939+
let test = cursor_test(
3940+
"\
3941+
def f[T](x: T):
3942+
x.<CURSOR>
3943+
",
3944+
);
3945+
test.assert_completions_include("__repr__");
3946+
}
3947+
38993948
// NOTE: The methods below are getting somewhat ridiculous.
39003949
// We should refactor this by converting to using a builder
39013950
// to set different modes. ---AG

crates/ty_python_semantic/src/types/ide_support.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::types::call::{CallArguments, MatchedArgument};
1414
use crate::types::signatures::Signature;
1515
use crate::types::{
1616
ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type,
17-
class::CodeGeneratorKind,
17+
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
1818
};
1919
use crate::{Db, HasType, NameKind, SemanticModel};
2020
use ruff_db::files::{File, FileRange};
@@ -177,6 +177,29 @@ impl<'db> AllMembers<'db> {
177177

178178
Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)),
179179

180+
Type::TypeVar(bound_typevar) => {
181+
match bound_typevar.typevar(db).bound_or_constraints(db) {
182+
None => {
183+
self.extend_with_type(db, Type::object());
184+
}
185+
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
186+
self.extend_with_type(db, bound);
187+
}
188+
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
189+
self.members.extend(
190+
constraints
191+
.elements(db)
192+
.iter()
193+
.map(|ty| AllMembers::of(db, *ty).members)
194+
.reduce(|acc, members| {
195+
acc.intersection(&members).cloned().collect()
196+
})
197+
.unwrap_or_default(),
198+
);
199+
}
200+
}
201+
}
202+
180203
Type::IntLiteral(_)
181204
| Type::BooleanLiteral(_)
182205
| Type::StringLiteral(_)
@@ -194,7 +217,6 @@ impl<'db> AllMembers<'db> {
194217
| Type::ProtocolInstance(_)
195218
| Type::SpecialForm(_)
196219
| Type::KnownInstance(_)
197-
| Type::TypeVar(_)
198220
| Type::BoundSuper(_)
199221
| Type::TypeIs(_) => match ty.to_meta_type(db) {
200222
Type::ClassLiteral(class_literal) => {

0 commit comments

Comments
 (0)