[ty] Induct into instances and subclasses when finding and applying generics (#18052)

We were not inducting into instance types and subclass-of types when
looking for legacy typevars, nor when apply specializations.

This addresses
https://github.com/astral-sh/ruff/pull/17832#discussion_r2081502056

```py
from __future__ import annotations
from typing import TypeVar, Any, reveal_type

S = TypeVar("S")

class Foo[T]:
    def method(self, other: Foo[S]) -> Foo[T | S]: ...  # type: ignore[invalid-return-type]

def f(x: Foo[Any], y: Foo[Any]):
    reveal_type(x.method(y))  # revealed: `Foo[Any | S]`, but should be `Foo[Any]`
```

We were not detecting that `S` made `method` generic, since we were not
finding it when searching the function signature for legacy typevars.
This commit is contained in:
Douglas Creager 2025-05-12 21:53:11 -04:00 committed by GitHub
parent 7e9b0df18a
commit f301931159
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 269 additions and 68 deletions

View file

@ -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())

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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(_) => {}
}
}

View file

@ -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<TypeVarInstance<'db>>,
) {
self.specialization(db).find_legacy_typevars(db, typevars);
}
}
impl<'db> From<GenericAlias<'db>> 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<TypeVarInstance<'db>>,
) {
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

View file

@ -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<TypeVarInstance<'db>>,
) {
self.class.find_legacy_typevars(db, typevars);
}
}
impl<'db> From<NominalInstanceType<'db>> 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<TypeVarInstance<'db>>,
) {
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<TypeVarInstance<'db>>,
) {
self.0.find_legacy_typevars(db, typevars);
}
pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> {
self.0
}

View file

@ -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<TypeVarInstance<'db>>,
) {
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<TypeVarInstance<'db>>,
) {
self.ty.find_legacy_typevars(db, typevars);
}
}
/// A single member of a protocol interface.

View file

@ -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<TypeVarInstance<'db>>,
) {
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,