[ty] Provide completions on TypeVars (#20943)

## Summary

closes https://github.com/astral-sh/ty/issues/1370

## Test Plan

New snapshot tests
This commit is contained in:
David Peter 2025-10-17 20:05:20 +02:00 committed by GitHub
parent c7e2bfd759
commit 6e7ff07065
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 2 deletions

View file

@ -3896,6 +3896,55 @@ print(t'''{Foo} and Foo.zqzq<CURSOR>
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
}
#[test]
fn typevar_with_upper_bound() {
let test = cursor_test(
"\
def f[T: str](msg: T):
msg.<CURSOR>
",
);
test.assert_completions_include("upper");
test.assert_completions_include("capitalize");
}
#[test]
fn typevar_with_constraints() {
// Test TypeVar with constraints
let test = cursor_test(
"\
from typing import TypeVar
class A:
only_on_a: int
on_a_and_b: str
class B:
only_on_b: float
on_a_and_b: str
T = TypeVar('T', A, B)
def f(x: T):
x.<CURSOR>
",
);
test.assert_completions_include("on_a_and_b");
test.assert_completions_do_not_include("only_on_a");
test.assert_completions_do_not_include("only_on_b");
}
#[test]
fn typevar_without_bounds_or_constraints() {
let test = cursor_test(
"\
def f[T](x: T):
x.<CURSOR>
",
);
test.assert_completions_include("__repr__");
}
// NOTE: The methods below are getting somewhat ridiculous.
// We should refactor this by converting to using a builder
// to set different modes. ---AG

View file

@ -14,7 +14,7 @@ use crate::types::call::{CallArguments, MatchedArgument};
use crate::types::signatures::Signature;
use crate::types::{
ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type,
class::CodeGeneratorKind,
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, HasType, NameKind, SemanticModel};
use ruff_db::files::{File, FileRange};
@ -177,6 +177,29 @@ impl<'db> AllMembers<'db> {
Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)),
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => {
self.extend_with_type(db, Type::object());
}
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
self.extend_with_type(db, bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
self.members.extend(
constraints
.elements(db)
.iter()
.map(|ty| AllMembers::of(db, *ty).members)
.reduce(|acc, members| {
acc.intersection(&members).cloned().collect()
})
.unwrap_or_default(),
);
}
}
}
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
@ -194,7 +217,6 @@ impl<'db> AllMembers<'db> {
| Type::ProtocolInstance(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => match ty.to_meta_type(db) {
Type::ClassLiteral(class_literal) => {