diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 89ae6ff685..9bd395b6c5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2484,7 +2484,7 @@ impl<'db> Type<'db> { disjointness_visitor, ), - (Type::KnownInstance(left), right) => left.instance_fallback(db).has_relation_to_impl( + (Type::KnownInstance(left), right) => left.has_relation_to_impl( db, right, inferable, @@ -2628,6 +2628,10 @@ impl<'db> Type<'db> { first.is_equivalent_to_impl(db, second, inferable, visitor) } + (Type::KnownInstance(first), Type::KnownInstance(second)) => { + ConstraintSet::from(first.is_equivalent_to(db, second)) + } + (Type::Union(first), Type::Union(second)) => { first.is_equivalent_to_impl(db, second, inferable, visitor) } @@ -8121,6 +8125,78 @@ impl<'db> KnownInstanceType<'db> { self.class(db).is_subclass_of(db, class) } + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + match (self, other) { + (Self::TypeVar(self_typevar), Self::TypeVar(other_typevar)) => { + self_typevar.is_same_typevar_as(db, other_typevar) + } + ( + Self::SubscriptedProtocol(_) + | Self::SubscriptedGeneric(_) + | Self::TypeVar(_) + | Self::TypeAliasType(_) + | Self::Deprecated(_) + | Self::Field(_) + | Self::ConstraintSet(_) + | Self::UnionType(_) + | Self::Literal(_) + | Self::Annotated(_) + | Self::TypeGenericAlias(_) + | Self::NewType(_), + Self::SubscriptedProtocol(_) + | Self::SubscriptedGeneric(_) + | Self::TypeVar(_) + | Self::TypeAliasType(_) + | Self::Deprecated(_) + | Self::Field(_) + | Self::ConstraintSet(_) + | Self::UnionType(_) + | Self::Literal(_) + | Self::Annotated(_) + | Self::TypeGenericAlias(_) + | Self::NewType(_), + ) => self == other, + } + } + + fn has_relation_to_impl( + self, + db: &'db dyn Db, + target: Type<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + ) -> ConstraintSet<'db> { + match (self, target) { + (Self::TypeVar(self_typevar), Type::KnownInstance(Self::TypeVar(other_typevar))) => { + ConstraintSet::from(self_typevar.is_same_typevar_as(db, other_typevar)) + } + ( + Self::SubscriptedProtocol(_) + | Self::SubscriptedGeneric(_) + | Self::TypeVar(_) + | Self::TypeAliasType(_) + | Self::Deprecated(_) + | Self::Field(_) + | Self::ConstraintSet(_) + | Self::UnionType(_) + | Self::Literal(_) + | Self::Annotated(_) + | Self::TypeGenericAlias(_) + | Self::NewType(_), + _, + ) => self.instance_fallback(db).has_relation_to_impl( + db, + target, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ), + } + } + /// Return the repr of the symbol at runtime fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct KnownInstanceRepr<'db> { @@ -8730,6 +8806,12 @@ impl<'db> TypeVarInstance<'db> { BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context)) } + /// Returns whether two typevars represent the same logical typevar, regardless of e.g. + /// differences in their bounds or constraints due to materialization. + pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { + self.identity(db) == other.identity(db) + } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { self.identity(db).name(db) } @@ -12541,14 +12623,39 @@ static_assertions::assert_eq_size!(Type, [u8; 16]); #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::db::tests::{TestDbBuilder, setup_db}; - use crate::place::{typing_extensions_symbol, typing_symbol}; + use crate::db::tests::{TestDb, TestDbBuilder, setup_db}; + use crate::place::{ConsideredDefinitions, symbol, typing_extensions_symbol, typing_symbol}; use crate::semantic_index::FileScopeId; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast::PythonVersion; use test_case::test_case; + #[track_caller] + pub(crate) fn get_symbol<'db>( + db: &'db TestDb, + file_name: &str, + scopes: &[&str], + symbol_name: &str, + ) -> Place<'db> { + let file = system_path_to_file(db, file_name).expect("file to exist"); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let mut file_scope_id = FileScopeId::global(); + let mut scope = file_scope_id.to_scope_id(db, file); + for expected_scope_name in scopes { + file_scope_id = index + .child_scopes(file_scope_id) + .next() + .unwrap_or_else(|| panic!("scope of {expected_scope_name}")) + .0; + scope = file_scope_id.to_scope_id(db, file); + assert_eq!(scope.name(db, &module), *expected_scope_name); + } + + symbol(db, scope, symbol_name, ConsideredDefinitions::EndOfScope).place + } + /// Explicitly test for Python version <3.13 and >=3.13, to ensure that /// the fallback to `typing_extensions` is working correctly. /// See [`KnownClass::canonical_module`] for more information. @@ -12699,6 +12806,40 @@ pub(crate) mod tests { assert_eq!(intersection.display(&db).to_string(), "Never"); } + #[test] + fn lazy_eager_typevar_equivalence() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + r#" + def f[T = int](): ... + "#, + ) + .unwrap(); + + let lazy_ty = get_symbol(&db, "/src/a.py", &["f"], "T").expect_type(); + let Type::KnownInstance(KnownInstanceType::TypeVar(lazy_typevar)) = lazy_ty else { + panic!("unexpected type {}", lazy_ty.display(&db)); + }; + assert_eq!( + lazy_typevar._default(&db), + Some(TypeVarDefaultEvaluation::Lazy) + ); + + let eager_ty = lazy_ty.normalized(&db); + let Type::KnownInstance(KnownInstanceType::TypeVar(eager_typevar)) = eager_ty else { + panic!("unexpected type {}", eager_ty.display(&db)); + }; + assert!(matches!( + eager_typevar._default(&db), + Some(TypeVarDefaultEvaluation::Eager(_)) + )); + + assert!(lazy_ty.is_equivalent_to(&db, eager_ty)); + assert!(lazy_ty.is_assignable_to(&db, eager_ty)); + assert!(eager_ty.is_assignable_to(&db, lazy_ty)); + } + #[test] fn type_alias_variance() { use crate::db::tests::TestDb; diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 9f6ae996be..167e4fa5a1 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -1,10 +1,9 @@ use super::builder::TypeInferenceBuilder; use crate::db::tests::{TestDb, setup_db}; -use crate::place::symbol; -use crate::place::{ConsideredDefinitions, Place, global_symbol}; +use crate::place::global_symbol; use crate::semantic_index::definition::Definition; -use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map}; +use crate::types::tests::get_symbol; use crate::types::{KnownClass, KnownInstanceType, check_types}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, system_path_to_file}; @@ -13,31 +12,6 @@ use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_ use super::*; -#[track_caller] -fn get_symbol<'db>( - db: &'db TestDb, - file_name: &str, - scopes: &[&str], - symbol_name: &str, -) -> Place<'db> { - let file = system_path_to_file(db, file_name).expect("file to exist"); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); - let mut file_scope_id = FileScopeId::global(); - let mut scope = file_scope_id.to_scope_id(db, file); - for expected_scope_name in scopes { - file_scope_id = index - .child_scopes(file_scope_id) - .next() - .unwrap_or_else(|| panic!("scope of {expected_scope_name}")) - .0; - scope = file_scope_id.to_scope_id(db, file); - assert_eq!(scope.name(db, &module), *expected_scope_name); - } - - symbol(db, scope, symbol_name, ConsideredDefinitions::EndOfScope).place -} - #[track_caller] fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { let messages: Vec<&str> = diagnostics