Skip to content

Commit 812d07b

Browse files
committed
Use assignment definition as typevar binding context
1 parent 401e72c commit 812d07b

File tree

4 files changed

+71
-17
lines changed

4 files changed

+71
-17
lines changed

crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,13 @@ def _(
191191
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
192192
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
193193
# TODO should be Unknown | int
194-
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
194+
reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
195195
# TODO should be int | Unknown
196-
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
196+
reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
197197
# TODO should be Unknown | None
198-
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
198+
reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None
199199
# TODO should be None | Unknown
200-
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
200+
reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar
201201
```
202202

203203
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -391,17 +391,17 @@ AnnotatedType = Annotated[T, "tag"]
391391
TransparentAlias = T
392392
MyOptional = T | None
393393

394-
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
395-
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
396-
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
394+
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
395+
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
397396
reveal_type(MyType) # revealed: GenericAlias
398-
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
399-
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
400-
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
397+
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
398+
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
399+
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
401400
reveal_type(ListOrTuple) # revealed: types.UnionType
402401
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
403402
reveal_type(MyCallable) # revealed: GenericAlias
404403
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
404+
# TODO: This should ideally be `T@TransparentAlias`
405405
reveal_type(TransparentAlias) # revealed: typing.TypeVar
406406
reveal_type(MyOptional) # revealed: types.UnionType
407407

@@ -445,7 +445,7 @@ U = TypeVar("U")
445445

446446
DictStrTo = MyDict[str, U]
447447

448-
reveal_type(DictStrTo) # revealed: <class 'dict[str, typing.TypeVar]'>
448+
reveal_type(DictStrTo) # revealed: <class 'dict[str, U@DictStrTo]'>
449449

450450
def _(
451451
dict_str_to_int: DictStrTo[int],
@@ -480,7 +480,7 @@ A generic implicit type alias can also be used in another generic implicit type
480480
```py
481481
MyOtherList = MyList[T]
482482

483-
reveal_type(MyOtherList) # revealed: <class 'list[typing.TypeVar]'>
483+
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
484484

485485
def _(
486486
list_of_ints: MyOtherList[int],
@@ -498,11 +498,11 @@ def _(
498498
my_callable: MyCallable,
499499
):
500500
# TODO: Should be `list[Unknown]`
501-
reveal_type(my_list) # revealed: list[typing.TypeVar]
501+
reveal_type(my_list) # revealed: list[T@MyList]
502502
# TODO: Should be `dict[Unknown, Unknown]`
503-
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
503+
reveal_type(my_dict) # revealed: dict[T@MyDict, U@MyDict]
504504
# TODO: Should be `(...) -> Unknown`
505-
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
505+
reveal_type(my_callable) # revealed: (...) -> T@MyCallable
506506
```
507507

508508
(Generic) implicit type aliases can be used as base classes:
@@ -552,6 +552,23 @@ def _(
552552
reveal_type(dict_too_few_args) # revealed: Unknown
553553
```
554554

555+
Trying to specialize a non-name node results in an error:
556+
557+
```py
558+
from ty_extensions import TypeOf
559+
560+
IntOrStr = int | str
561+
562+
def this_does_not_work() -> TypeOf[IntOrStr]:
563+
raise NotImplementedError()
564+
565+
def _(
566+
# error: [invalid-type-form] "Cannot specialize a non-name node in a type expression"
567+
specialized: this_does_not_work()[int],
568+
):
569+
reveal_type(specialized) # revealed: Unknown
570+
```
571+
555572
## `Literal`s
556573

557574
We also support `typing.Literal` in implicit type aliases.

crates/ty_python_semantic/src/semantic_index/definition.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ impl<'db> Definition<'db> {
9090
.to_string(),
9191
)
9292
}
93+
DefinitionKind::Assignment(assignment) => {
94+
let target_node = assignment.target.node(&module);
95+
target_node
96+
.as_name_expr()
97+
.map(|name_expr| name_expr.id.as_str().to_string())
98+
}
9399
_ => None,
94100
}
95101
}

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4739,6 +4739,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
47394739
unpacked.expression_type(target)
47404740
}
47414741
TargetKind::Single => {
4742+
// This could be an implicit type alias (OptionalList = list[T] | None). Use the definition
4743+
// of `OptionalList` as the typevar binding context while inferring the RHS (`list[T] | None`),
4744+
// in order to bind `T@OptionalList`.
4745+
let previous_typevar_binding_context =
4746+
self.typevar_binding_context.replace(definition);
4747+
47424748
let value_ty = if let Some(standalone_expression) = self.index.try_expression(value)
47434749
{
47444750
self.infer_standalone_expression_impl(value, standalone_expression, tcx)
@@ -4777,6 +4783,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
47774783
self.infer_expression(value, tcx)
47784784
};
47794785

4786+
self.typevar_binding_context = previous_typevar_binding_context;
4787+
47804788
// `TYPE_CHECKING` is a special variable that should only be assigned `False`
47814789
// at runtime, but is always considered `True` in type checking.
47824790
// See mdtest/known_constants.md#user-defined-type_checking for details.

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use itertools::Either;
22
use ruff_python_ast as ast;
33

44
use super::{DeferredExpressionState, TypeInferenceBuilder};
5-
use crate::FxOrderSet;
65
use crate::types::diagnostic::{
76
self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form,
87
report_invalid_arguments_to_callable,
@@ -16,6 +15,7 @@ use crate::types::{
1615
KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType,
1716
Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type,
1817
};
18+
use crate::{FxOrderSet, ResolvedDefinition, definitions_for_name};
1919

2020
/// Type expressions
2121
impl<'db> TypeInferenceBuilder<'db, '_> {
@@ -723,9 +723,32 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
723723
) -> Type<'db> {
724724
let db = self.db();
725725

726+
let Some(value) = subscript.value.as_name_expr() else {
727+
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
728+
builder.into_diagnostic("Cannot specialize a non-name node in a type expression");
729+
}
730+
return Type::unknown();
731+
};
732+
733+
// TODO: This is an expensive call to an API that was never meant to be called from
734+
// type inference. We plan to rework how `in_type_expression` works in the future.
735+
// This new approach will make this call unnecessary, so for now, we accept the hit
736+
// in performance.
737+
let definitions = definitions_for_name(self.db(), self.file(), value);
738+
let Some(type_alias_definition) =
739+
definitions.iter().find_map(ResolvedDefinition::definition)
740+
else {
741+
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
742+
builder.into_diagnostic(
743+
"Cannot specialize implicit type alias with unknown definition",
744+
);
745+
}
746+
return Type::unknown();
747+
};
748+
726749
let generic_type_alias = value_ty.apply_type_mapping(
727750
db,
728-
&TypeMapping::BindLegacyTypevars(BindingContext::Synthetic),
751+
&TypeMapping::BindLegacyTypevars(BindingContext::Definition(type_alias_definition)),
729752
TypeContext::default(),
730753
);
731754

0 commit comments

Comments
 (0)