Skip to content

Commit f301931

Browse files
authored
[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 #17832 (comment) ```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.
1 parent 7e9b0df commit f301931

File tree

9 files changed

+269
-68
lines changed

9 files changed

+269
-68
lines changed

crates/ruff_benchmark/benches/ty.rs

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -59,40 +59,7 @@ type KeyDiagnosticFields = (
5959
Severity,
6060
);
6161

62-
// left: [
63-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to function `skip_until` is incorrect", Error),
64-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to function `skip_until` is incorrect", Error),
65-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to function `skip_until` is incorrect", Error),
66-
// ]
67-
//right: [
68-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(8224..8254), "Argument to this function is incorrect", Error),
69-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(16914..16948), "Argument to this function is incorrect", Error),
70-
// (Lint(LintName("invalid-argument-type")), Some("/src/tomllib/_parser.py"), Some(17319..17363), "Argument to this function is incorrect", Error),
71-
// ]
72-
73-
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
74-
(
75-
DiagnosticId::lint("invalid-argument-type"),
76-
Some("/src/tomllib/_parser.py"),
77-
Some(8224..8254),
78-
"Argument to function `skip_until` is incorrect",
79-
Severity::Error,
80-
),
81-
(
82-
DiagnosticId::lint("invalid-argument-type"),
83-
Some("/src/tomllib/_parser.py"),
84-
Some(16914..16948),
85-
"Argument to function `skip_until` is incorrect",
86-
Severity::Error,
87-
),
88-
(
89-
DiagnosticId::lint("invalid-argument-type"),
90-
Some("/src/tomllib/_parser.py"),
91-
Some(17319..17363),
92-
"Argument to function `skip_until` is incorrect",
93-
Severity::Error,
94-
),
95-
];
62+
static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[];
9663

9764
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
9865
SystemPathBuf::from("src").join(file.name())

crates/ty_python_semantic/resources/mdtest/annotations/self.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Shape:
1919
reveal_type(self) # revealed: Self
2020
return self
2121

22-
def nested_type(self) -> list[Self]:
22+
def nested_type(self: Self) -> list[Self]:
2323
return [self]
2424

2525
def nested_func(self: Self) -> Self:
@@ -33,9 +33,7 @@ class Shape:
3333
reveal_type(self) # revealed: Unknown
3434
return self
3535

36-
# TODO: should be `list[Shape]`
37-
reveal_type(Shape().nested_type()) # revealed: list[Self]
38-
36+
reveal_type(Shape().nested_type()) # revealed: list[Shape]
3937
reveal_type(Shape().nested_func()) # revealed: Shape
4038

4139
class Circle(Shape):

crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,76 @@ reveal_type(f("string")) # revealed: Literal["string"]
6666
## Inferring “deep” generic parameter types
6767

6868
The matching up of call arguments and discovery of constraints on typevars can be a recursive
69-
process for arbitrarily-nested generic types in parameters.
69+
process for arbitrarily-nested generic classes and protocols in parameters.
70+
71+
TODO: Note that we can currently only infer a specialization for a generic protocol when the
72+
argument _explicitly_ implements the protocol by listing it as a base class.
7073

7174
```py
72-
from typing import TypeVar
75+
from typing import Protocol, TypeVar
7376

7477
T = TypeVar("T")
7578

76-
def f(x: list[T]) -> T:
79+
class CanIndex(Protocol[T]):
80+
def __getitem__(self, index: int) -> T: ...
81+
82+
class ExplicitlyImplements(CanIndex[T]): ...
83+
84+
def takes_in_list(x: list[T]) -> list[T]:
85+
return x
86+
87+
def takes_in_protocol(x: CanIndex[T]) -> T:
7788
return x[0]
7889

79-
# TODO: revealed: float
80-
reveal_type(f([1.0, 2.0])) # revealed: Unknown
90+
def deep_list(x: list[str]) -> None:
91+
# TODO: revealed: list[str]
92+
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
93+
# TODO: revealed: str
94+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
95+
96+
def deeper_list(x: list[set[str]]) -> None:
97+
# TODO: revealed: list[set[str]]
98+
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
99+
# TODO: revealed: set[str]
100+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
101+
102+
def deep_explicit(x: ExplicitlyImplements[str]) -> None:
103+
# TODO: revealed: str
104+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
105+
106+
def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
107+
# TODO: revealed: set[str]
108+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
109+
110+
def takes_in_type(x: type[T]) -> type[T]:
111+
return x
112+
113+
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
114+
```
115+
116+
This also works when passing in arguments that are subclasses of the parameter type.
117+
118+
```py
119+
class Sub(list[int]): ...
120+
class GenericSub(list[T]): ...
121+
122+
# TODO: revealed: list[int]
123+
reveal_type(takes_in_list(Sub())) # revealed: list[Unknown]
124+
# TODO: revealed: int
125+
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
126+
127+
# TODO: revealed: list[str]
128+
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown]
129+
# TODO: revealed: str
130+
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown
131+
132+
class ExplicitSub(ExplicitlyImplements[int]): ...
133+
class ExplicitGenericSub(ExplicitlyImplements[T]): ...
134+
135+
# TODO: revealed: int
136+
reveal_type(takes_in_protocol(ExplicitSub())) # revealed: Unknown
137+
# TODO: revealed: str
138+
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: Unknown
81139
```
82140

83141
## Inferring a bound typevar

crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,76 @@ reveal_type(f("string")) # revealed: Literal["string"]
6161
## Inferring “deep” generic parameter types
6262

6363
The matching up of call arguments and discovery of constraints on typevars can be a recursive
64-
process for arbitrarily-nested generic types in parameters.
64+
process for arbitrarily-nested generic classes and protocols in parameters.
65+
66+
TODO: Note that we can currently only infer a specialization for a generic protocol when the
67+
argument _explicitly_ implements the protocol by listing it as a base class.
6568

6669
```py
67-
def f[T](x: list[T]) -> T:
70+
from typing import Protocol, TypeVar
71+
72+
S = TypeVar("S")
73+
74+
class CanIndex(Protocol[S]):
75+
def __getitem__(self, index: int) -> S: ...
76+
77+
class ExplicitlyImplements[T](CanIndex[T]): ...
78+
79+
def takes_in_list[T](x: list[T]) -> list[T]:
80+
return x
81+
82+
def takes_in_protocol[T](x: CanIndex[T]) -> T:
6883
return x[0]
6984

70-
# TODO: revealed: float
71-
reveal_type(f([1.0, 2.0])) # revealed: Unknown
85+
def deep_list(x: list[str]) -> None:
86+
# TODO: revealed: list[str]
87+
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
88+
# TODO: revealed: str
89+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
90+
91+
def deeper_list(x: list[set[str]]) -> None:
92+
# TODO: revealed: list[set[str]]
93+
reveal_type(takes_in_list(x)) # revealed: list[Unknown]
94+
# TODO: revealed: set[str]
95+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
96+
97+
def deep_explicit(x: ExplicitlyImplements[str]) -> None:
98+
# TODO: revealed: str
99+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
100+
101+
def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
102+
# TODO: revealed: set[str]
103+
reveal_type(takes_in_protocol(x)) # revealed: Unknown
104+
105+
def takes_in_type[T](x: type[T]) -> type[T]:
106+
return x
107+
108+
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
109+
```
110+
111+
This also works when passing in arguments that are subclasses of the parameter type.
112+
113+
```py
114+
class Sub(list[int]): ...
115+
class GenericSub[T](list[T]): ...
116+
117+
# TODO: revealed: list[int]
118+
reveal_type(takes_in_list(Sub())) # revealed: list[Unknown]
119+
# TODO: revealed: int
120+
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
121+
122+
# TODO: revealed: list[str]
123+
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[Unknown]
124+
# TODO: revealed: str
125+
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown
126+
127+
class ExplicitSub(ExplicitlyImplements[int]): ...
128+
class ExplicitGenericSub[T](ExplicitlyImplements[T]): ...
129+
130+
# TODO: revealed: int
131+
reveal_type(takes_in_protocol(ExplicitSub())) # revealed: Unknown
132+
# TODO: revealed: str
133+
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: Unknown
72134
```
73135

74136
## Inferring a bound typevar

crates/ty_python_semantic/src/types.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5048,7 +5048,7 @@ impl<'db> Type<'db> {
50485048
),
50495049

50505050
Type::ProtocolInstance(instance) => {
5051-
Type::ProtocolInstance(instance.apply_specialization(db, type_mapping))
5051+
Type::ProtocolInstance(instance.apply_type_mapping(db, type_mapping))
50525052
}
50535053

50545054
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
@@ -5080,12 +5080,13 @@ impl<'db> Type<'db> {
50805080
}
50815081

50825082
Type::GenericAlias(generic) => {
5083-
let specialization = generic
5084-
.specialization(db)
5085-
.apply_type_mapping(db, type_mapping);
5086-
Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization))
5083+
Type::GenericAlias(generic.apply_type_mapping(db, type_mapping))
50875084
}
50885085

5086+
Type::SubclassOf(subclass_of) => Type::SubclassOf(
5087+
subclass_of.apply_type_mapping(db, type_mapping),
5088+
),
5089+
50895090
Type::PropertyInstance(property) => {
50905091
Type::PropertyInstance(property.apply_type_mapping(db, type_mapping))
50915092
}
@@ -5125,9 +5126,6 @@ impl<'db> Type<'db> {
51255126
// explicitly (via a subscript expression) or implicitly (via a call), and not because
51265127
// some other generic context's specialization is applied to it.
51275128
| Type::ClassLiteral(_)
5128-
// SubclassOf contains a ClassType, which has already been specialized if needed, like
5129-
// above with BoundMethod's self_instance.
5130-
| Type::SubclassOf(_)
51315129
| Type::IntLiteral(_)
51325130
| Type::BooleanLiteral(_)
51335131
| Type::LiteralString
@@ -5202,7 +5200,19 @@ impl<'db> Type<'db> {
52025200
}
52035201

52045202
Type::GenericAlias(alias) => {
5205-
alias.specialization(db).find_legacy_typevars(db, typevars);
5203+
alias.find_legacy_typevars(db, typevars);
5204+
}
5205+
5206+
Type::NominalInstance(instance) => {
5207+
instance.find_legacy_typevars(db, typevars);
5208+
}
5209+
5210+
Type::ProtocolInstance(instance) => {
5211+
instance.find_legacy_typevars(db, typevars);
5212+
}
5213+
5214+
Type::SubclassOf(subclass_of) => {
5215+
subclass_of.find_legacy_typevars(db, typevars);
52065216
}
52075217

52085218
Type::Dynamic(_)
@@ -5215,15 +5225,12 @@ impl<'db> Type<'db> {
52155225
| Type::DataclassTransformer(_)
52165226
| Type::ModuleLiteral(_)
52175227
| Type::ClassLiteral(_)
5218-
| Type::SubclassOf(_)
52195228
| Type::IntLiteral(_)
52205229
| Type::BooleanLiteral(_)
52215230
| Type::LiteralString
52225231
| Type::StringLiteral(_)
52235232
| Type::BytesLiteral(_)
52245233
| Type::BoundSuper(_)
5225-
| Type::NominalInstance(_)
5226-
| Type::ProtocolInstance(_)
52275234
| Type::KnownInstance(_) => {}
52285235
}
52295236
}

crates/ty_python_semantic/src/types/class.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::types::generics::{GenericContext, Specialization, TypeMapping};
1212
use crate::types::signatures::{Parameter, Parameters};
1313
use crate::types::{
1414
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature,
15+
TypeVarInstance,
1516
};
1617
use crate::{
1718
module_resolver::file_to_module,
@@ -31,7 +32,7 @@ use crate::{
3132
definition_expression_type, CallArgumentTypes, CallError, CallErrorKind, DynamicType,
3233
MetaclassCandidate, TupleType, UnionBuilder, UnionType,
3334
},
34-
Db, KnownModule, Program,
35+
Db, FxOrderSet, KnownModule, Program,
3536
};
3637
use indexmap::IndexSet;
3738
use itertools::Itertools as _;
@@ -167,13 +168,25 @@ impl<'db> GenericAlias<'db> {
167168
self.origin(db).definition(db)
168169
}
169170

170-
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self {
171+
pub(super) fn apply_type_mapping<'a>(
172+
self,
173+
db: &'db dyn Db,
174+
type_mapping: TypeMapping<'a, 'db>,
175+
) -> Self {
171176
Self::new(
172177
db,
173178
self.origin(db),
174179
self.specialization(db).apply_type_mapping(db, type_mapping),
175180
)
176181
}
182+
183+
pub(super) fn find_legacy_typevars(
184+
self,
185+
db: &'db dyn Db,
186+
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
187+
) {
188+
self.specialization(db).find_legacy_typevars(db, typevars);
189+
}
177190
}
178191

179192
impl<'db> From<GenericAlias<'db>> for Type<'db> {
@@ -262,6 +275,17 @@ impl<'db> ClassType<'db> {
262275
}
263276
}
264277

278+
pub(super) fn find_legacy_typevars(
279+
self,
280+
db: &'db dyn Db,
281+
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
282+
) {
283+
match self {
284+
Self::NonGeneric(_) => {}
285+
Self::Generic(generic) => generic.find_legacy_typevars(db, typevars),
286+
}
287+
}
288+
265289
/// Iterate over the [method resolution order] ("MRO") of the class.
266290
///
267291
/// If the MRO could not be accurately resolved, this method falls back to iterating

0 commit comments

Comments
 (0)