diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 419fcae322..bda25506f2 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -284,13 +284,13 @@ impl GotoTarget<'_> { // When asking the type of a callable, usually you want the callable itself? // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) GotoTarget::Call { callable, .. } => callable.inferred_type(model), + GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model), // TODO: Support identifier targets GotoTarget::PatternMatchRest(_) | GotoTarget::PatternKeywordArgument(_) | GotoTarget::PatternMatchStarName(_) | GotoTarget::PatternMatchAsName(_) | GotoTarget::ImportModuleComponent { .. } - | GotoTarget::TypeParamTypeVarName(_) | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 45b82ca8b0..241078c76f 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -6,7 +6,7 @@ use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; -use ty_python_semantic::types::Type; +use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance}; use ty_python_semantic::{DisplaySettings, SemanticModel}; pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { @@ -20,7 +20,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option typevar + .bind_pep695(db) + .map_or(HoverContent::Type(ty, None), |typevar| { + HoverContent::Type(Type::TypeVar(typevar), Some(typevar.variance(db))) + }), + Type::TypeVar(typevar) => HoverContent::Type(ty, Some(typevar.variance(db))), + _ => HoverContent::Type(ty, None), + }); + } contents.extend(docs); if contents.is_empty() { @@ -110,7 +117,7 @@ impl fmt::Display for DisplayHover<'_> { #[derive(Debug, Clone, Eq, PartialEq)] pub enum HoverContent<'db> { - Type(Type<'db>), + Type(Type<'db>, Option), Docstring(Docstring), } @@ -133,13 +140,24 @@ pub(crate) struct DisplayHoverContent<'a, 'db> { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.content { - HoverContent::Type(ty) => self - .kind - .fenced_code_block( - ty.display_with(self.db, DisplaySettings::default().multiline()), - "python", - ) - .fmt(f), + HoverContent::Type(ty, variance) => { + let variance = match variance { + Some(TypeVarVariance::Covariant) => " (covariant)", + Some(TypeVarVariance::Contravariant) => " (contravariant)", + Some(TypeVarVariance::Invariant) => " (invariant)", + Some(TypeVarVariance::Bivariant) => " (bivariant)", + None => "", + }; + self.kind + .fenced_code_block( + format!( + "{}{variance}", + ty.display_with(self.db, DisplaySettings::default().multiline()) + ), + "python", + ) + .fmt(f) + } HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f), } } @@ -1590,10 +1608,10 @@ def ab(a: int, *, c: int): ); assert_snapshot!(test.hover(), @r" - T@Alias + T@Alias (invariant) --------------------------------------------- ```python - T@Alias + T@Alias (invariant) ``` --------------------------------------------- info[hover]: Hovered content is @@ -2058,6 +2076,444 @@ def ab(a: int, *, c: int): assert_snapshot!(test.hover(), @"Hover provided no content"); } + #[test] + fn hover_class_typevar_variance() { + let test = cursor_test( + r#" + class Covariant[T]: + def get(self) -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Covariant (covariant) + --------------------------------------------- + ```python + T@Covariant (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:17 + | + 2 | class Covariant[T]: + | ^- Cursor offset + | | + | source + 3 | def get(self) -> T: + 4 | raise ValueError + | + "); + + let test = cursor_test( + r#" + class Covariant[T]: + def get(self) -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Covariant (covariant) + --------------------------------------------- + ```python + T@Covariant (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:22 + | + 2 | class Covariant[T]: + 3 | def get(self) -> T: + | ^- Cursor offset + | | + | source + 4 | raise ValueError + | + "); + + let test = cursor_test( + r#" + class Contravariant[T]: + def set(self, x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Contravariant (contravariant) + --------------------------------------------- + ```python + T@Contravariant (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:21 + | + 2 | class Contravariant[T]: + | ^- Cursor offset + | | + | source + 3 | def set(self, x: T): + 4 | pass + | + "); + + let test = cursor_test( + r#" + class Contravariant[T]: + def set(self, x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Contravariant (contravariant) + --------------------------------------------- + ```python + T@Contravariant (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:22 + | + 2 | class Contravariant[T]: + 3 | def set(self, x: T): + | ^- Cursor offset + | | + | source + 4 | pass + | + "); + } + + #[test] + fn hover_function_typevar_variance() { + let test = cursor_test( + r#" + def covariant[T]() -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@covariant (covariant) + --------------------------------------------- + ```python + T@covariant (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:15 + | + 2 | def covariant[T]() -> T: + | ^- Cursor offset + | | + | source + 3 | raise ValueError + | + "); + + let test = cursor_test( + r#" + def covariant[T]() -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@covariant (covariant) + --------------------------------------------- + ```python + T@covariant (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:23 + | + 2 | def covariant[T]() -> T: + | ^- Cursor offset + | | + | source + 3 | raise ValueError + | + "); + + let test = cursor_test( + r#" + def contravariant[T](x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@contravariant (contravariant) + --------------------------------------------- + ```python + T@contravariant (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:19 + | + 2 | def contravariant[T](x: T): + | ^- Cursor offset + | | + | source + 3 | pass + | + "); + + let test = cursor_test( + r#" + def contravariant[T](x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@contravariant (contravariant) + --------------------------------------------- + ```python + T@contravariant (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:25 + | + 2 | def contravariant[T](x: T): + | ^- Cursor offset + | | + | source + 3 | pass + | + "); + } + + #[test] + fn hover_type_alias_typevar_variance() { + let test = cursor_test( + r#" + type List[T] = list[T] + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@List (invariant) + --------------------------------------------- + ```python + T@List (invariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:11 + | + 2 | type List[T] = list[T] + | ^- Cursor offset + | | + | source + | + "); + + let test = cursor_test( + r#" + type List[T] = list[T] + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@List (invariant) + --------------------------------------------- + ```python + T@List (invariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:21 + | + 2 | type List[T] = list[T] + | ^- Cursor offset + | | + | source + | + "); + + let test = cursor_test( + r#" + type Tuple[T] = tuple[T] + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Tuple (covariant) + --------------------------------------------- + ```python + T@Tuple (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:12 + | + 2 | type Tuple[T] = tuple[T] + | ^- Cursor offset + | | + | source + | + "); + + let test = cursor_test( + r#" + type Tuple[T] = tuple[T] + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@Tuple (covariant) + --------------------------------------------- + ```python + T@Tuple (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:23 + | + 2 | type Tuple[T] = tuple[T] + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_legacy_typevar_variance() { + let test = cursor_test( + r#" + from typing import TypeVar + + T = TypeVar('T', covariant=True) + + def covariant() -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + typing.TypeVar + --------------------------------------------- + ```python + typing.TypeVar + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:1 + | + 2 | from typing import TypeVar + 3 | + 4 | T = TypeVar('T', covariant=True) + | ^- Cursor offset + | | + | source + 5 | + 6 | def covariant() -> T: + | + "); + + let test = cursor_test( + r#" + from typing import TypeVar + + T = TypeVar('T', covariant=True) + + def covariant() -> T: + raise ValueError + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@covariant (covariant) + --------------------------------------------- + ```python + T@covariant (covariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:6:20 + | + 4 | T = TypeVar('T', covariant=True) + 5 | + 6 | def covariant() -> T: + | ^- Cursor offset + | | + | source + 7 | raise ValueError + | + "); + + let test = cursor_test( + r#" + from typing import TypeVar + + T = TypeVar('T', contravariant=True) + + def contravariant(x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + typing.TypeVar + --------------------------------------------- + ```python + typing.TypeVar + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:1 + | + 2 | from typing import TypeVar + 3 | + 4 | T = TypeVar('T', contravariant=True) + | ^- Cursor offset + | | + | source + 5 | + 6 | def contravariant(x: T): + | + "); + + let test = cursor_test( + r#" + from typing import TypeVar + + T = TypeVar('T', contravariant=True) + + def contravariant(x: T): + pass + "#, + ); + + assert_snapshot!(test.hover(), @r" + T@contravariant (contravariant) + --------------------------------------------- + ```python + T@contravariant (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:6:22 + | + 4 | T = TypeVar('T', contravariant=True) + 5 | + 6 | def contravariant(x: T): + | ^- Cursor offset + | | + | source + 7 | pass + | + "); + } + impl CursorTest { fn hover(&self) -> String { use std::fmt::Write; diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index 62c7ff2a97..c7c42241a3 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -11,6 +11,7 @@ use crate::{ semantic_index::{ SemanticIndex, reachability_constraints::ScopedReachabilityConstraintId, semantic_index, }, + types::{GenericContext, binding_type, infer_definition_types}, }; /// A cross-module identifier of a scope that can be used as a salsa query parameter. @@ -430,6 +431,38 @@ impl NodeWithScopeKind { pub(crate) fn expect_type_alias(&self) -> &AstNodeRef { self.as_type_alias().expect("expected type alias") } + + pub(crate) fn generic_context<'db>( + &self, + db: &'db dyn Db, + index: &SemanticIndex<'db>, + ) -> Option> { + match self { + NodeWithScopeKind::Class(class) => { + let definition = index.expect_single_definition(class); + binding_type(db, definition) + .as_class_literal()? + .generic_context(db) + } + NodeWithScopeKind::Function(function) => { + let definition = index.expect_single_definition(function); + infer_definition_types(db, definition) + .undecorated_type() + .expect("function should have undecorated type") + .as_function_literal()? + .last_definition_signature(db) + .generic_context + } + NodeWithScopeKind::TypeAlias(type_alias) => { + let definition = index.expect_single_definition(type_alias); + binding_type(db, definition) + .as_type_alias()? + .as_pep_695_type_alias()? + .generic_context(db) + } + _ => None, + } + } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index eb50b50366..beb2c7f968 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -479,6 +479,7 @@ impl_binding_has_ty_def!(ast::StmtClassDef); impl_binding_has_ty_def!(ast::Parameter); impl_binding_has_ty_def!(ast::ParameterWithDefault); impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler); +impl_binding_has_ty_def!(ast::TypeParamTypeVar); impl HasType for ast::Alias { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9327a8f0ec..d9a51ce5b9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -67,7 +67,8 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::tuple::{TupleSpec, TupleSpecBuilder}; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; -use crate::types::variance::{TypeVarVariance, VarianceInferable}; +pub use crate::types::variance::TypeVarVariance; +use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; use crate::unpack::EvaluationMode; use crate::{Db, FxOrderSet, Module, Program}; @@ -8394,6 +8395,21 @@ impl<'db> TypeVarInstance<'db> { _ => None, } } + + pub fn bind_pep695(self, db: &'db dyn Db) -> Option> { + if self.identity(db).kind(db) != TypeVarKind::Pep695 { + return None; + } + let typevar_definition = self.definition(db)?; + let index = semantic_index(db, typevar_definition.file(db)); + let (_, child) = index + .child_scopes(typevar_definition.file_scope(db)) + .next()?; + child + .node() + .generic_context(db, index)? + .binds_typevar(db, self) + } } #[allow(clippy::ref_option)] @@ -8544,7 +8560,7 @@ impl<'db> BoundTypeVarInstance<'db> { } } - pub(crate) fn variance(self, db: &'db dyn Db) -> TypeVarVariance { + pub fn variance(self, db: &'db dyn Db) -> TypeVarVariance { self.variance_with_polarity(db, TypeVarVariance::Covariant) } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 9a9864bab2..f401e0df3f 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -11,7 +11,6 @@ use crate::semantic_index::{SemanticIndex, semantic_index}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSet; -use crate::types::infer::infer_definition_types; use crate::types::instance::{Protocol, ProtocolInstanceType}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; @@ -21,8 +20,7 @@ use crate::types::{ FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, binding_type, declaration_type, - walk_bound_type_var_type, + TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, }; use crate::{Db, FxIndexSet, FxOrderMap, FxOrderSet}; @@ -35,31 +33,7 @@ pub(crate) fn enclosing_generic_contexts<'db>( ) -> impl Iterator> { index .ancestor_scopes(scope) - .filter_map(|(_, ancestor_scope)| match ancestor_scope.node() { - NodeWithScopeKind::Class(class) => { - let definition = index.expect_single_definition(class); - binding_type(db, definition) - .as_class_literal()? - .generic_context(db) - } - NodeWithScopeKind::Function(function) => { - let definition = index.expect_single_definition(function); - infer_definition_types(db, definition) - .undecorated_type() - .expect("function should have undecorated type") - .as_function_literal()? - .last_definition_signature(db) - .generic_context - } - NodeWithScopeKind::TypeAlias(type_alias) => { - let definition = index.expect_single_definition(type_alias); - binding_type(db, definition) - .as_type_alias()? - .as_pep_695_type_alias()? - .generic_context(db) - } - _ => None, - }) + .filter_map(|(_, ancestor_scope)| ancestor_scope.node().generic_context(db, index)) } /// Binds an unbound typevar.