diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 1599a2bed5..8cfdb18099 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3896,6 +3896,55 @@ print(t'''{Foo} and Foo.zqzq assert_snapshot!(test.completions_without_builtins(), @""); } + #[test] + fn typevar_with_upper_bound() { + let test = cursor_test( + "\ +def f[T: str](msg: T): + msg. +", + ); + 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. +", + ); + 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. +", + ); + 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 diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index ea436b4163..d02011390b 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -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) => {