diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 3bb28fe0ce..83c58e5e82 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -59,40 +59,7 @@ type KeyDiagnosticFields = ( Severity, ); -// left: [ -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to function `skip_until` is incorrect", Error), -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to function `skip_until` is incorrect", Error), -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to function `skip_until` is incorrect", Error), -// ] -//right: [ -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to this function is incorrect", Error), -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to this function is incorrect", Error), -// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to this function is incorrect", Error), -// ] - -static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(8224..8254), - "Argument to function `skip_until` is incorrect", - Severity::Error, - ), - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(16914..16948), - "Argument to function `skip_until` is incorrect", - Severity::Error, - ), - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(17319..17363), - "Argument to function `skip_until` is incorrect", - Severity::Error, - ), -]; +static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[]; fn tomllib_path(file: &TestFile) -> SystemPathBuf { SystemPathBuf::from("src").join(file.name()) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 5ffbe1ae8b..8a99fac64c 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -19,7 +19,7 @@ class Shape: reveal_type(self) # revealed: Self return self - def nested_type(self) -> list[Self]: + def nested_type(self: Self) -> list[Self]: return [self] def nested_func(self: Self) -> Self: @@ -33,9 +33,7 @@ class Shape: reveal_type(self) # revealed: Unknown return self -# TODO: should be `list[Shape]` -reveal_type(Shape().nested_type()) # revealed: list[Self] - +reveal_type(Shape().nested_type()) # revealed: list[Shape] reveal_type(Shape().nested_func()) # revealed: Shape class Circle(Shape): diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 9f20d754cb..62ffaf561e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -66,18 +66,76 @@ reveal_type(f("string")) # revealed: Literal["string"] ## Inferring “deep” generic parameter types The matching up of call arguments and discovery of constraints on typevars can be a recursive -process for arbitrarily-nested generic types in parameters. +process for arbitrarily-nested generic classes and protocols in parameters. + +TODO: Note that we can currently only infer a specialization for a generic protocol when the +argument _explicitly_ implements the protocol by listing it as a base class. ```py -from typing import TypeVar +from typing import Protocol, TypeVar T = TypeVar("T") -def f(x: list[T]) -> T: +class CanIndex(Protocol[T]): + def __getitem__(self, index: int) -> T: ... + +class ExplicitlyImplements(CanIndex[T]): ... + +def takes_in_list(x: list[T]) -> list[T]: + return x + +def takes_in_protocol(x: CanIndex[T]) -> T: return x[0] -# TODO: revealed: float -reveal_type(f([1.0, 2.0])) # revealed: Unknown +def deep_list(x: list[str]) -> None: + # TODO: revealed: list[str] + reveal_type(takes_in_list(x)) # revealed: list[Unknown] + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_list(x: list[set[str]]) -> None: + # TODO: revealed: list[set[str]] + reveal_type(takes_in_list(x)) # revealed: list[Unknown] + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deep_explicit(x: ExplicitlyImplements[str]) -> None: + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def takes_in_type(x: type[T]) -> type[T]: + return x + +reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +``` + +This also works when passing in arguments that are subclasses of the parameter type. + +```py +class Sub(list[int]): ... +class GenericSub(list[T]): ... + +# TODO: revealed: list[int] +reveal_type(takes_in_list(Sub())) # revealed: list[Unknown] +# TODO: revealed: int +reveal_type(takes_in_protocol(Sub())) # revealed: Unknown + +# TODO: revealed: list[str] +reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown] +# TODO: revealed: str +reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + +class ExplicitSub(ExplicitlyImplements[int]): ... +class ExplicitGenericSub(ExplicitlyImplements[T]): ... + +# TODO: revealed: int +reveal_type(takes_in_protocol(ExplicitSub())) # revealed: Unknown +# TODO: revealed: str +reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: Unknown ``` ## Inferring a bound typevar diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index ffd208920c..31f8569e5c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -61,14 +61,76 @@ reveal_type(f("string")) # revealed: Literal["string"] ## Inferring “deep” generic parameter types The matching up of call arguments and discovery of constraints on typevars can be a recursive -process for arbitrarily-nested generic types in parameters. +process for arbitrarily-nested generic classes and protocols in parameters. + +TODO: Note that we can currently only infer a specialization for a generic protocol when the +argument _explicitly_ implements the protocol by listing it as a base class. ```py -def f[T](x: list[T]) -> T: +from typing import Protocol, TypeVar + +S = TypeVar("S") + +class CanIndex(Protocol[S]): + def __getitem__(self, index: int) -> S: ... + +class ExplicitlyImplements[T](CanIndex[T]): ... + +def takes_in_list[T](x: list[T]) -> list[T]: + return x + +def takes_in_protocol[T](x: CanIndex[T]) -> T: return x[0] -# TODO: revealed: float -reveal_type(f([1.0, 2.0])) # revealed: Unknown +def deep_list(x: list[str]) -> None: + # TODO: revealed: list[str] + reveal_type(takes_in_list(x)) # revealed: list[Unknown] + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_list(x: list[set[str]]) -> None: + # TODO: revealed: list[set[str]] + reveal_type(takes_in_list(x)) # revealed: list[Unknown] + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deep_explicit(x: ExplicitlyImplements[str]) -> None: + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def takes_in_type[T](x: type[T]) -> type[T]: + return x + +reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +``` + +This also works when passing in arguments that are subclasses of the parameter type. + +```py +class Sub(list[int]): ... +class GenericSub[T](list[T]): ... + +# TODO: revealed: list[int] +reveal_type(takes_in_list(Sub())) # revealed: list[Unknown] +# TODO: revealed: int +reveal_type(takes_in_protocol(Sub())) # revealed: Unknown + +# TODO: revealed: list[str] +reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown] +# TODO: revealed: str +reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + +class ExplicitSub(ExplicitlyImplements[int]): ... +class ExplicitGenericSub[T](ExplicitlyImplements[T]): ... + +# TODO: revealed: int +reveal_type(takes_in_protocol(ExplicitSub())) # revealed: Unknown +# TODO: revealed: str +reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: Unknown ``` ## Inferring a bound typevar diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cd48fa9877..bf91743bb2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5048,7 +5048,7 @@ impl<'db> Type<'db> { ), Type::ProtocolInstance(instance) => { - Type::ProtocolInstance(instance.apply_specialization(db, type_mapping)) + Type::ProtocolInstance(instance.apply_type_mapping(db, type_mapping)) } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { @@ -5080,12 +5080,13 @@ impl<'db> Type<'db> { } Type::GenericAlias(generic) => { - let specialization = generic - .specialization(db) - .apply_type_mapping(db, type_mapping); - Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) + Type::GenericAlias(generic.apply_type_mapping(db, type_mapping)) } + Type::SubclassOf(subclass_of) => Type::SubclassOf( + subclass_of.apply_type_mapping(db, type_mapping), + ), + Type::PropertyInstance(property) => { Type::PropertyInstance(property.apply_type_mapping(db, type_mapping)) } @@ -5125,9 +5126,6 @@ impl<'db> Type<'db> { // explicitly (via a subscript expression) or implicitly (via a call), and not because // some other generic context's specialization is applied to it. | Type::ClassLiteral(_) - // SubclassOf contains a ClassType, which has already been specialized if needed, like - // above with BoundMethod's self_instance. - | Type::SubclassOf(_) | Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::LiteralString @@ -5202,7 +5200,19 @@ impl<'db> Type<'db> { } Type::GenericAlias(alias) => { - alias.specialization(db).find_legacy_typevars(db, typevars); + alias.find_legacy_typevars(db, typevars); + } + + Type::NominalInstance(instance) => { + instance.find_legacy_typevars(db, typevars); + } + + Type::ProtocolInstance(instance) => { + instance.find_legacy_typevars(db, typevars); + } + + Type::SubclassOf(subclass_of) => { + subclass_of.find_legacy_typevars(db, typevars); } Type::Dynamic(_) @@ -5215,15 +5225,12 @@ impl<'db> Type<'db> { | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) - | Type::SubclassOf(_) | Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::LiteralString | Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::BoundSuper(_) - | Type::NominalInstance(_) - | Type::ProtocolInstance(_) | Type::KnownInstance(_) => {} } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index e99844a9d8..150915f9eb 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -12,6 +12,7 @@ use crate::types::generics::{GenericContext, Specialization, TypeMapping}; use crate::types::signatures::{Parameter, Parameters}; use crate::types::{ CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature, + TypeVarInstance, }; use crate::{ module_resolver::file_to_module, @@ -31,7 +32,7 @@ use crate::{ definition_expression_type, CallArgumentTypes, CallError, CallErrorKind, DynamicType, MetaclassCandidate, TupleType, UnionBuilder, UnionType, }, - Db, KnownModule, Program, + Db, FxOrderSet, KnownModule, Program, }; use indexmap::IndexSet; use itertools::Itertools as _; @@ -167,13 +168,25 @@ impl<'db> GenericAlias<'db> { self.origin(db).definition(db) } - fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self { + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: TypeMapping<'a, 'db>, + ) -> Self { Self::new( db, self.origin(db), self.specialization(db).apply_type_mapping(db, type_mapping), ) } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.specialization(db).find_legacy_typevars(db, typevars); + } } impl<'db> From> for Type<'db> { @@ -262,6 +275,17 @@ impl<'db> ClassType<'db> { } } + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Self::NonGeneric(_) => {} + Self::Generic(generic) => generic.find_legacy_typevars(db, typevars), + } + } + /// Iterate over the [method resolution order] ("MRO") of the class. /// /// If the MRO could not be accurately resolved, this method falls back to iterating diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index b492a55952..a232400670 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -4,8 +4,8 @@ use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::symbol::{Symbol, SymbolAndQualifiers}; use crate::types::generics::TypeMapping; -use crate::types::ClassLiteral; -use crate::Db; +use crate::types::{ClassLiteral, TypeVarInstance}; +use crate::{Db, FxOrderSet}; pub(super) use synthesized_protocol::SynthesizedProtocolType; @@ -132,6 +132,14 @@ impl<'db> NominalInstanceType<'db> { class: self.class.apply_type_mapping(db, type_mapping), } } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.class.find_legacy_typevars(db, typevars); + } } impl<'db> From> for Type<'db> { @@ -270,7 +278,7 @@ impl<'db> ProtocolInstanceType<'db> { } } - pub(super) fn apply_specialization<'a>( + pub(super) fn apply_type_mapping<'a>( self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>, @@ -284,6 +292,21 @@ impl<'db> ProtocolInstanceType<'db> { )), } } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self.0 { + Protocol::FromClass(class) => { + class.find_legacy_typevars(db, typevars); + } + Protocol::Synthesized(synthesized) => { + synthesized.find_legacy_typevars(db, typevars); + } + } + } } /// An enumeration of the two kinds of protocol types: those that originate from a class @@ -310,9 +333,10 @@ impl<'db> Protocol<'db> { } mod synthesized_protocol { - use crate::db::Db; use crate::types::generics::TypeMapping; use crate::types::protocol_class::ProtocolInterface; + use crate::types::TypeVarInstance; + use crate::{Db, FxOrderSet}; /// A "synthesized" protocol type that is dissociated from a class definition in source code. /// @@ -339,6 +363,14 @@ mod synthesized_protocol { Self(self.0.specialized_and_normalized(db, type_mapping)) } + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.0.find_legacy_typevars(db, typevars); + } + pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> { self.0 } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index cf7a05ebf9..c29be0bd3e 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -5,10 +5,12 @@ use itertools::{Either, Itertools}; use ruff_python_ast::name::Name; use crate::{ - db::Db, semantic_index::{symbol_table, use_def_map}, symbol::{symbol_from_bindings, symbol_from_declarations}, - types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers}, + types::{ + ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers, TypeVarInstance, + }, + {Db, FxOrderSet}, }; impl<'db> ClassLiteral<'db> { @@ -188,6 +190,21 @@ impl<'db> ProtocolInterface<'db> { Self::SelfReference => Self::SelfReference, } } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Self::Members(members) => { + for data in members.inner(db).values() { + data.find_legacy_typevars(db, typevars); + } + } + Self::SelfReference => {} + } + } } #[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] @@ -210,6 +227,14 @@ impl<'db> ProtocolMemberData<'db> { qualifiers: self.qualifiers, } } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.ty.find_legacy_typevars(db, typevars); + } } /// A single member of a protocol interface. diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 9eccbdb2c7..127540abca 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,6 +1,8 @@ use crate::symbol::SymbolAndQualifiers; +use crate::types::generics::TypeMapping; +use crate::{Db, FxOrderSet}; -use super::{ClassType, Db, DynamicType, KnownClass, MemberLookupPolicy, Type}; +use super::{ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeVarInstance}; /// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] @@ -66,6 +68,32 @@ impl<'db> SubclassOfType<'db> { !self.is_dynamic() } + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: TypeMapping<'a, 'db>, + ) -> Self { + match self.subclass_of { + SubclassOfInner::Class(class) => Self { + subclass_of: SubclassOfInner::Class(class.apply_type_mapping(db, type_mapping)), + }, + SubclassOfInner::Dynamic(_) => self, + } + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self.subclass_of { + SubclassOfInner::Class(class) => { + class.find_legacy_typevars(db, typevars); + } + SubclassOfInner::Dynamic(_) => {} + } + } + pub(crate) fn find_name_in_mro_with_policy( self, db: &'db dyn Db,