mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] fix deferred name loading in PEP695 generic classes/functions (#19888)
## Summary For PEP 695 generic functions and classes, there is an extra "type params scope" (a child of the outer scope, and wrapping the body scope) in which the type parameters are defined; class bases and function parameter/return annotations are resolved in that type-params scope. This PR fixes some longstanding bugs in how we resolve name loads from inside these PEP 695 type parameter scopes, and also defers type inference of PEP 695 typevar bounds/constraints/default, so we can handle cycles without panicking. We were previously treating these type-param scopes as lazy nested scopes, which is wrong. In fact they are eager nested scopes; the class `C` here inherits `int`, not `str`, and previously we got that wrong: ```py Base = int class C[T](Base): ... Base = str ``` But certain syntactic positions within type param scopes (typevar bounds/constraints/defaults) are lazy at runtime, and we should use deferred name resolution for them. This also means they can have cycles; in order to handle that without panicking in type inference, we need to actually defer their type inference until after we have constructed the `TypeVarInstance`. PEP 695 does specify that typevar bounds and constraints cannot be generic, and that typevar defaults can only reference prior typevars, not later ones. This reduces the scope of (valid from the type-system perspective) cycles somewhat, although cycles are still possible (e.g. `class C[T: list[C]]`). And this is a type-system-only restriction; from the runtime perspective an "invalid" case like `class C[T: T]` actually works fine. I debated whether to implement the PEP 695 restrictions as a way to avoid some cycles up-front, but I ended up deciding against that; I'd rather model the runtime name-resolution semantics accurately, and implement the PEP 695 restrictions as a separate diagnostic on top. (This PR doesn't yet implement those diagnostics, thus some `# TODO: error` in the added tests.) Introducing the possibility of cyclic typevars made typevar display potentially stack overflow. For now I've handled this by simply removing typevar details (bounds/constraints/default) from typevar display. This impacts display of two kinds of types. If you `reveal_type(T)` on an unbound `T` you now get just `typing.TypeVar` instead of `typing.TypeVar("T", ...)` where `...` is the bound/constraints/default. This matches pyright and mypy; pyrefly uses `type[TypeVar[T]]` which seems a bit confusing, but does include the name. (We could easily include the name without cycle issues, if there's a syntax we like for that.) It also means that displaying a generic function type like `def f[T: int](x: T) -> T: ...` now displays as `f[T](x: T) -> T` instead of `f[T: int](x: T) -> T`. This matches pyright and pyrefly; mypy does include bound/constraints/defaults of typevars in function/callable type display. If we wanted to add this, we would either need to thread a visitor through all the type display code, or add a `decycle` type transformation that replaced recursive reoccurrence of a type with a marker. ## Test Plan Added mdtests and modified existing tests to improve their correctness. After this PR, there's only a single remaining py-fuzzer seed in the 0-500 range that panics! (Before this PR, there were 10; the fuzzer likes to generate cyclic PEP 695 syntax.) ## Ecosystem report It's all just the changes to `TypeVar` display.
This commit is contained in:
parent
baadb5a78d
commit
5a570c8e6d
22 changed files with 429 additions and 239 deletions
|
@ -167,7 +167,7 @@ pub(crate) fn attribute_scopes<'db, 's>(
|
|||
|
||||
ChildrenIter::new(index, class_scope_id).filter_map(move |(child_scope_id, scope)| {
|
||||
let (function_scope_id, function_scope) =
|
||||
if scope.node().scope_kind() == ScopeKind::Annotation {
|
||||
if scope.node().scope_kind() == ScopeKind::TypeParams {
|
||||
// This could be a generic method with a type-params scope.
|
||||
// Go one level deeper to find the function scope. The first
|
||||
// descendant is the (potential) function scope.
|
||||
|
@ -597,8 +597,8 @@ impl<'a> Iterator for VisibleAncestorsIter<'a> {
|
|||
// Skip class scopes for subsequent scopes (following Python's lexical scoping rules)
|
||||
// Exception: type parameter scopes can see names defined in an immediately-enclosing class scope
|
||||
if scope.kind() == ScopeKind::Class {
|
||||
// Allow type parameter scopes to see their immediately-enclosing class scope exactly once
|
||||
if self.starting_scope_kind.is_type_parameter() && self.yielded_count == 2 {
|
||||
// Allow annotation scopes to see their immediately-enclosing class scope exactly once
|
||||
if self.starting_scope_kind.is_annotation() && self.yielded_count == 2 {
|
||||
return Some((scope_id, scope));
|
||||
}
|
||||
continue;
|
||||
|
@ -1317,7 +1317,7 @@ def func[T]():
|
|||
panic!("expected one child scope");
|
||||
};
|
||||
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::TypeParams);
|
||||
assert_eq!(
|
||||
ann_scope_id.to_scope_id(&db, file).name(&db, &module),
|
||||
"func"
|
||||
|
@ -1361,7 +1361,7 @@ class C[T]:
|
|||
panic!("expected one child scope");
|
||||
};
|
||||
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::TypeParams);
|
||||
assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C");
|
||||
let ann_table = index.place_table(ann_scope_id);
|
||||
assert_eq!(names(&ann_table), vec!["T"]);
|
||||
|
|
|
@ -199,7 +199,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
|||
|
||||
match self.scopes[parent.file_scope_id].kind() {
|
||||
ScopeKind::Class => Some(parent.file_scope_id),
|
||||
ScopeKind::Annotation => {
|
||||
ScopeKind::TypeParams => {
|
||||
// If the function is generic, the parent scope is an annotation scope.
|
||||
// In this case, we need to go up one level higher to find the class scope.
|
||||
let grandparent = scopes_rev.next()?;
|
||||
|
@ -2637,7 +2637,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
|
|||
ScopeKind::Comprehension
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Annotation => {}
|
||||
| ScopeKind::TypeParams => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
@ -2652,7 +2652,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
|
|||
ScopeKind::Comprehension
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Annotation => {}
|
||||
| ScopeKind::TypeParams => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
@ -2664,7 +2664,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
|
|||
match scope.kind() {
|
||||
ScopeKind::Class | ScopeKind::Comprehension => return false,
|
||||
ScopeKind::Function | ScopeKind::Lambda => return true,
|
||||
ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::Annotation => {}
|
||||
ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::TypeParams => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
|
|
@ -702,6 +702,13 @@ impl DefinitionKind<'_> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef<ast::TypeParamTypeVar>> {
|
||||
match self {
|
||||
DefinitionKind::TypeVar(type_var) => Some(type_var),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the definition target.
|
||||
///
|
||||
/// A definition target would mainly be the node representing the place being defined i.e.,
|
||||
|
|
|
@ -29,8 +29,8 @@ impl<'db> ScopeId<'db> {
|
|||
self.node(db).scope_kind().is_function_like()
|
||||
}
|
||||
|
||||
pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool {
|
||||
self.node(db).scope_kind().is_type_parameter()
|
||||
pub(crate) fn is_annotation(self, db: &'db dyn Db) -> bool {
|
||||
self.node(db).scope_kind().is_annotation()
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
|
@ -200,7 +200,7 @@ impl ScopeLaziness {
|
|||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ScopeKind {
|
||||
Module,
|
||||
Annotation,
|
||||
TypeParams,
|
||||
Class,
|
||||
Function,
|
||||
Lambda,
|
||||
|
@ -215,18 +215,18 @@ impl ScopeKind {
|
|||
|
||||
pub(crate) const fn laziness(self) -> ScopeLaziness {
|
||||
match self {
|
||||
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => ScopeLaziness::Eager,
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias => ScopeLaziness::Lazy,
|
||||
ScopeKind::Module
|
||||
| ScopeKind::Class
|
||||
| ScopeKind::Comprehension
|
||||
| ScopeKind::TypeParams => ScopeLaziness::Eager,
|
||||
ScopeKind::Function | ScopeKind::Lambda | ScopeKind::TypeAlias => ScopeLaziness::Lazy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn visibility(self) -> ScopeVisibility {
|
||||
match self {
|
||||
ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public,
|
||||
ScopeKind::Annotation
|
||||
ScopeKind::TypeParams
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
|
@ -239,7 +239,7 @@ impl ScopeKind {
|
|||
// symbol table also uses the term "function-like" for these scopes.
|
||||
matches!(
|
||||
self,
|
||||
ScopeKind::Annotation
|
||||
ScopeKind::TypeParams
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias
|
||||
|
@ -255,8 +255,8 @@ impl ScopeKind {
|
|||
matches!(self, ScopeKind::Module)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_type_parameter(self) -> bool {
|
||||
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
|
||||
pub(crate) const fn is_annotation(self) -> bool {
|
||||
matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_non_lambda_function(self) -> bool {
|
||||
|
@ -388,7 +388,7 @@ impl NodeWithScopeKind {
|
|||
Self::Lambda(_) => ScopeKind::Lambda,
|
||||
Self::FunctionTypeParameters(_)
|
||||
| Self::ClassTypeParameters(_)
|
||||
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
|
||||
| Self::TypeAliasTypeParameters(_) => ScopeKind::TypeParams,
|
||||
Self::TypeAlias(_) => ScopeKind::TypeAlias,
|
||||
Self::ListComprehension(_)
|
||||
| Self::SetComprehension(_)
|
||||
|
|
|
@ -5291,7 +5291,7 @@ impl<'db> Type<'db> {
|
|||
db,
|
||||
Name::new(format!("{}'instance", typevar.name(db))),
|
||||
None,
|
||||
Some(bound_or_constraints),
|
||||
Some(bound_or_constraints.into()),
|
||||
typevar.variance(db),
|
||||
None,
|
||||
typevar.kind(db),
|
||||
|
@ -5482,7 +5482,7 @@ impl<'db> Type<'db> {
|
|||
db,
|
||||
ast::name::Name::new_static("Self"),
|
||||
Some(class_definition),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(instance)),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(instance).into()),
|
||||
TypeVarVariance::Invariant,
|
||||
None,
|
||||
TypeVarKind::Implicit,
|
||||
|
@ -6439,9 +6439,7 @@ impl<'db> KnownInstanceType<'db> {
|
|||
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
|
||||
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
|
||||
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
|
||||
KnownInstanceType::TypeVar(typevar) => {
|
||||
write!(f, "typing.TypeVar({})", typevar.display(self.db))
|
||||
}
|
||||
KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"),
|
||||
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
|
||||
KnownInstanceType::Field(field) => {
|
||||
f.write_str("dataclasses.Field[")?;
|
||||
|
@ -6862,14 +6860,17 @@ pub struct TypeVarInstance<'db> {
|
|||
/// The type var's definition (None if synthesized)
|
||||
pub definition: Option<Definition<'db>>,
|
||||
|
||||
/// The upper bound or constraint on the type of this TypeVar
|
||||
bound_or_constraints: Option<TypeVarBoundOrConstraints<'db>>,
|
||||
/// The upper bound or constraint on the type of this TypeVar, if any. Don't use this field
|
||||
/// directly; use the `bound_or_constraints` (or `upper_bound` and `constraints`) methods
|
||||
/// instead (to evaluate any lazy bound or constraints).
|
||||
_bound_or_constraints: Option<TypeVarBoundOrConstraintsEvaluation<'db>>,
|
||||
|
||||
/// The variance of the TypeVar
|
||||
variance: TypeVarVariance,
|
||||
|
||||
/// The default type for this TypeVar
|
||||
default_ty: Option<Type<'db>>,
|
||||
/// The default type for this TypeVar, if any. Don't use this field directly, use the
|
||||
/// `default_type` method instead (to evaluate any lazy default).
|
||||
_default: Option<TypeVarDefaultEvaluation<'db>>,
|
||||
|
||||
pub kind: TypeVarKind,
|
||||
}
|
||||
|
@ -6885,11 +6886,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
|||
if let Some(bounds) = typevar.bound_or_constraints(db) {
|
||||
walk_type_var_bounds(db, bounds, visitor);
|
||||
}
|
||||
if let Some(default_type) = typevar.default_ty(db) {
|
||||
if let Some(default_type) = typevar.default_type(db) {
|
||||
visitor.visit_type(db, default_type);
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> TypeVarInstance<'db> {
|
||||
pub(crate) fn with_binding_context(
|
||||
self,
|
||||
|
@ -6919,15 +6921,50 @@ impl<'db> TypeVarInstance<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bound_or_constraints(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Option<TypeVarBoundOrConstraints<'db>> {
|
||||
self._bound_or_constraints(db).and_then(|w| match w {
|
||||
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
|
||||
Some(bound_or_constraints)
|
||||
}
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self.lazy_bound(db),
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self.lazy_constraints(db),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn default_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
self._default(db).and_then(|d| match d {
|
||||
TypeVarDefaultEvaluation::Eager(ty) => Some(ty),
|
||||
TypeVarDefaultEvaluation::Lazy => self.lazy_default(db),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db),
|
||||
self.definition(db),
|
||||
self.bound_or_constraints(db)
|
||||
.map(|b| b.normalized_impl(db, visitor)),
|
||||
self._bound_or_constraints(db)
|
||||
.and_then(|bound_or_constraints| match bound_or_constraints {
|
||||
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
|
||||
Some(bound_or_constraints.normalized_impl(db, visitor).into())
|
||||
}
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self
|
||||
.lazy_bound(db)
|
||||
.map(|bound| bound.normalized_impl(db, visitor).into()),
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self
|
||||
.lazy_constraints(db)
|
||||
.map(|constraints| constraints.normalized_impl(db, visitor).into()),
|
||||
}),
|
||||
self.variance(db),
|
||||
self.default_ty(db).map(|d| d.normalized_impl(db, visitor)),
|
||||
self._default(db).and_then(|default| match default {
|
||||
TypeVarDefaultEvaluation::Eager(ty) => Some(ty.normalized_impl(db, visitor).into()),
|
||||
TypeVarDefaultEvaluation::Lazy => self
|
||||
.lazy_default(db)
|
||||
.map(|ty| ty.normalized_impl(db, visitor).into()),
|
||||
}),
|
||||
self.kind(db),
|
||||
)
|
||||
}
|
||||
|
@ -6937,13 +6974,59 @@ impl<'db> TypeVarInstance<'db> {
|
|||
db,
|
||||
self.name(db),
|
||||
self.definition(db),
|
||||
self.bound_or_constraints(db)
|
||||
.map(|b| b.materialize(db, variance)),
|
||||
self._bound_or_constraints(db)
|
||||
.and_then(|bound_or_constraints| match bound_or_constraints {
|
||||
TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => {
|
||||
Some(bound_or_constraints.materialize(db, variance).into())
|
||||
}
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self
|
||||
.lazy_bound(db)
|
||||
.map(|bound| bound.materialize(db, variance).into()),
|
||||
TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self
|
||||
.lazy_constraints(db)
|
||||
.map(|constraints| constraints.materialize(db, variance).into()),
|
||||
}),
|
||||
self.variance(db),
|
||||
self.default_ty(db),
|
||||
self._default(db).and_then(|default| match default {
|
||||
TypeVarDefaultEvaluation::Eager(ty) => Some(ty.materialize(db, variance).into()),
|
||||
TypeVarDefaultEvaluation::Lazy => self
|
||||
.lazy_default(db)
|
||||
.map(|ty| ty.materialize(db, variance).into()),
|
||||
}),
|
||||
self.kind(db),
|
||||
)
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
fn lazy_bound(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
|
||||
let definition = self.definition(db)?;
|
||||
let module = parsed_module(db, definition.file(db)).load(db);
|
||||
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
|
||||
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(ty))
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
|
||||
let definition = self.definition(db)?;
|
||||
let module = parsed_module(db, definition.file(db)).load(db);
|
||||
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
|
||||
let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
|
||||
.into_union()?;
|
||||
Some(TypeVarBoundOrConstraints::Constraints(ty))
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
fn lazy_default(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
let definition = self.definition(db)?;
|
||||
let module = parsed_module(db, definition.file(db)).load(db);
|
||||
let typevar_node = definition.kind(db).as_typevar()?.node(&module);
|
||||
Some(definition_expression_type(
|
||||
db,
|
||||
definition,
|
||||
typevar_node.default.as_ref()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a type variable is bound and usable.
|
||||
|
@ -7008,10 +7091,10 @@ impl<'db> BoundTypeVarInstance<'db> {
|
|||
/// By using `U` in the generic class, it becomes bound, and so we have a
|
||||
/// `BoundTypeVarInstance`. As part of binding `U` we must also bind its default value
|
||||
/// (resulting in `T@C`).
|
||||
pub(crate) fn default_ty(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
pub(crate) fn default_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
let binding_context = self.binding_context(db);
|
||||
self.typevar(db)
|
||||
.default_ty(db)
|
||||
.default_type(db)
|
||||
.map(|ty| ty.apply_type_mapping(db, &TypeMapping::BindLegacyTypevars(binding_context)))
|
||||
}
|
||||
|
||||
|
@ -7054,6 +7137,38 @@ impl TypeVarVariance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a typevar default is eagerly specified or lazily evaluated.
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum TypeVarDefaultEvaluation<'db> {
|
||||
/// The default type is lazily evaluated.
|
||||
Lazy,
|
||||
/// The default type is eagerly specified.
|
||||
Eager(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> From<Type<'db>> for TypeVarDefaultEvaluation<'db> {
|
||||
fn from(value: Type<'db>) -> Self {
|
||||
TypeVarDefaultEvaluation::Eager(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a typevar bound/constraints is eagerly specified or lazily evaluated.
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum TypeVarBoundOrConstraintsEvaluation<'db> {
|
||||
/// There is a lazily-evaluated upper bound.
|
||||
LazyUpperBound,
|
||||
/// There is a lazily-evaluated set of constraints.
|
||||
LazyConstraints,
|
||||
/// The upper bound/constraints are eagerly specified.
|
||||
Eager(TypeVarBoundOrConstraints<'db>),
|
||||
}
|
||||
|
||||
impl<'db> From<TypeVarBoundOrConstraints<'db>> for TypeVarBoundOrConstraintsEvaluation<'db> {
|
||||
fn from(value: TypeVarBoundOrConstraints<'db>) -> Self {
|
||||
TypeVarBoundOrConstraintsEvaluation::Eager(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub enum TypeVarBoundOrConstraints<'db> {
|
||||
UpperBound(Type<'db>),
|
||||
|
|
|
@ -397,7 +397,7 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
Some("__default__") => {
|
||||
overload.set_return_type(
|
||||
typevar.default_ty(db).unwrap_or_else(|| {
|
||||
typevar.default_type(db).unwrap_or_else(|| {
|
||||
KnownClass::NoDefaultType.to_instance(db)
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -4520,7 +4520,9 @@ impl KnownClass {
|
|||
}
|
||||
|
||||
let bound_or_constraint = match (bound, constraints) {
|
||||
(Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)),
|
||||
(Some(bound), None) => {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(*bound).into())
|
||||
}
|
||||
|
||||
(None, Some(_constraints)) => {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here,
|
||||
|
@ -4536,7 +4538,7 @@ impl KnownClass {
|
|||
.map(|(_, ty)| ty)
|
||||
.collect::<Box<_>>(),
|
||||
);
|
||||
Some(TypeVarBoundOrConstraints::Constraints(elements))
|
||||
Some(TypeVarBoundOrConstraints::Constraints(elements).into())
|
||||
}
|
||||
|
||||
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and
|
||||
|
@ -4554,7 +4556,7 @@ impl KnownClass {
|
|||
Some(containing_assignment),
|
||||
bound_or_constraint,
|
||||
variance,
|
||||
*default,
|
||||
default.map(Into::into),
|
||||
TypeVarKind::Legacy,
|
||||
),
|
||||
)));
|
||||
|
|
|
@ -14,9 +14,8 @@ use crate::types::generics::{GenericContext, Specialization};
|
|||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::tuple::TupleSpec;
|
||||
use crate::types::{
|
||||
BoundTypeVarInstance, CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol,
|
||||
StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
UnionType, WrapperDescriptorKind,
|
||||
CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType,
|
||||
SubclassOfInner, Type, UnionType, WrapperDescriptorKind,
|
||||
};
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
|
@ -417,83 +416,6 @@ impl Display for DisplayGenericAlias<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'db> TypeVarInstance<'db> {
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayTypeVarInstance<'db> {
|
||||
DisplayTypeVarInstance { typevar: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayTypeVarInstance<'db> {
|
||||
typevar: TypeVarInstance<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayTypeVarInstance<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
display_quoted_string(self.typevar.name(self.db)).fmt(f)?;
|
||||
match self.typevar.bound_or_constraints(self.db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
write!(f, ", bound={}", bound.display(self.db))?;
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
for constraint in constraints.iter(self.db) {
|
||||
write!(f, ", {}", constraint.display(self.db))?;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(default_type) = self.typevar.default_ty(self.db) {
|
||||
write!(f, ", default={}", default_type.display(self.db))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> BoundTypeVarInstance<'db> {
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> DisplayBoundTypeVarInstance<'db> {
|
||||
DisplayBoundTypeVarInstance {
|
||||
bound_typevar: self,
|
||||
db,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayBoundTypeVarInstance<'db> {
|
||||
bound_typevar: BoundTypeVarInstance<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayBoundTypeVarInstance<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
// This looks very much like DisplayTypeVarInstance::fmt, but note that we have typevar
|
||||
// default values in a subtly different way: if the default value contains other typevars,
|
||||
// here those must be bound as well, whereas in DisplayTypeVarInstance they should not. See
|
||||
// BoundTypeVarInstance::default_ty for more details.
|
||||
let typevar = self.bound_typevar.typevar(self.db);
|
||||
f.write_str(typevar.name(self.db))?;
|
||||
match typevar.bound_or_constraints(self.db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
write!(f, ": {}", bound.display(self.db))?;
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
f.write_str(": (")?;
|
||||
for (idx, constraint) in constraints.iter(self.db).enumerate() {
|
||||
if idx > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
constraint.display(self.db).fmt(f)?;
|
||||
}
|
||||
f.write_char(')')?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(default_type) = self.bound_typevar.default_ty(self.db) {
|
||||
write!(f, " = {}", default_type.display(self.db))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> GenericContext<'db> {
|
||||
pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> {
|
||||
DisplayGenericContext {
|
||||
|
@ -545,7 +467,7 @@ impl Display for DisplayGenericContext<'_> {
|
|||
if idx > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
bound_typevar.display(self.db).fmt(f)?;
|
||||
f.write_str(bound_typevar.typevar(self.db).name(self.db))?;
|
||||
}
|
||||
f.write_char(']')
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ impl<'db> GenericContext<'db> {
|
|||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(default_ty) = bound_typevar.default_ty(db) {
|
||||
if let Some(default_ty) = bound_typevar.default_type(db) {
|
||||
parameter = parameter.with_default_type(default_ty);
|
||||
}
|
||||
parameter
|
||||
|
@ -337,7 +337,7 @@ impl<'db> GenericContext<'db> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let Some(default) = typevar.default_ty(db) else {
|
||||
let Some(default) = typevar.default_type(db) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
@ -756,7 +756,7 @@ impl<'db> SpecializationBuilder<'db> {
|
|||
self.types
|
||||
.get(variable)
|
||||
.copied()
|
||||
.unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown()))
|
||||
.unwrap_or(variable.default_type(self.db).unwrap_or(Type::unknown()))
|
||||
})
|
||||
.collect();
|
||||
// TODO Infer the tuple spec for a tuple type
|
||||
|
|
|
@ -121,8 +121,9 @@ use crate::types::{
|
|||
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard,
|
||||
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
|
||||
Parameters, SpecialFormType, SubclassOfType, Truthiness, Type, TypeAliasType,
|
||||
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
|
||||
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation,
|
||||
TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
|
||||
UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::unpack::{EvaluationMode, Unpack, UnpackPosition};
|
||||
use crate::util::diagnostics::format_enumeration;
|
||||
|
@ -1748,6 +1749,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
DefinitionKind::Class(class) => {
|
||||
self.infer_class_deferred(definition, class.node(self.module()));
|
||||
}
|
||||
DefinitionKind::TypeVar(typevar) => {
|
||||
self.infer_typevar_deferred(typevar.node(self.module()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -2243,6 +2247,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
self.infer_type_parameters(type_params);
|
||||
|
||||
if let Some(arguments) = class.arguments.as_deref() {
|
||||
let in_stub = self.in_stub();
|
||||
let previous_deferred_state =
|
||||
std::mem::replace(&mut self.deferred_state, in_stub.into());
|
||||
let mut call_arguments =
|
||||
CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| {
|
||||
let ty = self.infer_expression(splatted_value);
|
||||
|
@ -2251,6 +2258,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
});
|
||||
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
|
||||
self.deferred_state = previous_deferred_state;
|
||||
}
|
||||
|
||||
self.typevar_binding_context = previous_typevar_binding_context;
|
||||
|
@ -2271,7 +2279,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
self.typevar_binding_context.replace(binding_context);
|
||||
self.infer_return_type_annotation(
|
||||
function.returns.as_deref(),
|
||||
DeferredExpressionState::None,
|
||||
self.defer_annotations().into(),
|
||||
);
|
||||
self.infer_type_parameters(type_params);
|
||||
self.infer_parameters(&function.parameters);
|
||||
|
@ -2316,7 +2324,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
let class_scope = match parent_scope.kind() {
|
||||
ScopeKind::Class => parent_scope,
|
||||
ScopeKind::Annotation => {
|
||||
ScopeKind::TypeParams => {
|
||||
let class_scope_id = parent_scope.parent()?;
|
||||
let potentially_class_scope = self.index.scope(class_scope_id);
|
||||
|
||||
|
@ -2757,7 +2765,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
let annotated = self.infer_optional_annotation_expression(
|
||||
parameter.annotation.as_deref(),
|
||||
DeferredExpressionState::None,
|
||||
self.defer_annotations().into(),
|
||||
);
|
||||
|
||||
if let Some(qualifiers) = annotated.map(|annotated| annotated.qualifiers) {
|
||||
|
@ -2792,7 +2800,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
|
||||
self.infer_optional_annotation_expression(
|
||||
annotation.as_deref(),
|
||||
DeferredExpressionState::None,
|
||||
self.defer_annotations().into(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3402,44 +3410,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
{
|
||||
builder.into_diagnostic("TypeVar must have at least two constrained types");
|
||||
}
|
||||
self.infer_expression(expr);
|
||||
None
|
||||
} else {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
|
||||
// want to simplify the list of constraints like we do with the elements of an
|
||||
// actual union type.
|
||||
// TODO: Consider using a new `OneOfType` connective here instead, since that
|
||||
// more accurately represents the actual semantics of typevar constraints.
|
||||
let elements = UnionType::new(
|
||||
self.db(),
|
||||
elts.iter()
|
||||
.map(|expr| self.infer_type_expression(expr))
|
||||
.collect::<Box<[_]>>(),
|
||||
);
|
||||
let constraints = TypeVarBoundOrConstraints::Constraints(elements);
|
||||
// But when we construct an actual union type for the constraint expression as
|
||||
// a whole, we do use UnionType::from_elements to maintain the invariant that
|
||||
// all union types are simplified.
|
||||
self.store_expression_type(
|
||||
expr,
|
||||
UnionType::from_elements(self.db(), elements.elements(self.db())),
|
||||
);
|
||||
Some(constraints)
|
||||
Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints)
|
||||
}
|
||||
}
|
||||
Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound(
|
||||
self.infer_type_expression(expr),
|
||||
)),
|
||||
Some(_) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound),
|
||||
None => None,
|
||||
};
|
||||
let default_ty = self.infer_optional_type_expression(default.as_deref());
|
||||
if bound_or_constraint.is_some() || default.is_some() {
|
||||
self.deferred.insert(definition);
|
||||
}
|
||||
let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
|
||||
self.db(),
|
||||
&name.id,
|
||||
Some(definition),
|
||||
bound_or_constraint,
|
||||
TypeVarVariance::Invariant, // TODO: infer this
|
||||
default_ty,
|
||||
default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy),
|
||||
TypeVarKind::Pep695,
|
||||
)));
|
||||
self.add_declaration_with_binding(
|
||||
|
@ -3449,6 +3437,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
);
|
||||
}
|
||||
|
||||
fn infer_typevar_deferred(&mut self, node: &ast::TypeParamTypeVar) {
|
||||
let ast::TypeParamTypeVar {
|
||||
range: _,
|
||||
node_index: _,
|
||||
name: _,
|
||||
bound,
|
||||
default,
|
||||
} = node;
|
||||
match bound.as_deref() {
|
||||
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
|
||||
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
|
||||
// want to simplify the list of constraints like we do with the elements of an
|
||||
// actual union type.
|
||||
// TODO: Consider using a new `OneOfType` connective here instead, since that
|
||||
// more accurately represents the actual semantics of typevar constraints.
|
||||
let ty = Type::Union(UnionType::new(
|
||||
self.db(),
|
||||
elts.iter()
|
||||
.map(|expr| self.infer_type_expression(expr))
|
||||
.collect::<Box<[_]>>(),
|
||||
));
|
||||
self.store_expression_type(expr, ty);
|
||||
}
|
||||
Some(expr) => {
|
||||
self.infer_type_expression(expr);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
self.infer_optional_type_expression(default.as_deref());
|
||||
}
|
||||
|
||||
fn infer_paramspec_definition(
|
||||
&mut self,
|
||||
node: &ast::TypeParamParamSpec,
|
||||
|
@ -6573,7 +6592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let place_table = self.index.place_table(file_scope_id);
|
||||
let use_def = self.index.use_def_map(file_scope_id);
|
||||
|
||||
// If we're inferring types of deferred expressions, always treat them as public symbols
|
||||
// If we're inferring types of deferred expressions, look them up from end-of-scope.
|
||||
if self.is_deferred() {
|
||||
let place = if let Some(place_id) = place_table.place_id(expr) {
|
||||
place_from_bindings(db, use_def.all_reachable_bindings(place_id))
|
||||
|
@ -6684,11 +6703,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
// Class scopes are not visible to nested scopes, and we need to handle global
|
||||
// scope differently (because an unbound name there falls back to builtins), so
|
||||
// check only function-like scopes.
|
||||
// There is one exception to this rule: type parameter scopes can see
|
||||
// There is one exception to this rule: annotation scopes can see
|
||||
// names defined in an immediately-enclosing class scope.
|
||||
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
|
||||
|
||||
let is_immediately_enclosing_scope = scope.is_type_parameter(db)
|
||||
let is_immediately_enclosing_scope = scope.is_annotation(db)
|
||||
&& scope
|
||||
.scope(db)
|
||||
.parent()
|
||||
|
@ -9535,17 +9554,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
ty
|
||||
}
|
||||
|
||||
/// Similar to [`infer_type_expression`], but accepts an optional type expression and returns
|
||||
/// [`None`] if the expression is [`None`].
|
||||
///
|
||||
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression
|
||||
fn infer_optional_type_expression(
|
||||
&mut self,
|
||||
expression: Option<&ast::Expr>,
|
||||
) -> Option<Type<'db>> {
|
||||
expression.map(|expr| self.infer_type_expression(expr))
|
||||
}
|
||||
|
||||
/// Similar to [`infer_type_expression`], but accepts a [`DeferredExpressionState`].
|
||||
///
|
||||
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression
|
||||
|
@ -9560,6 +9568,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
annotation_ty
|
||||
}
|
||||
|
||||
/// Similar to [`infer_type_expression`], but accepts an optional expression.
|
||||
///
|
||||
/// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression_with_state
|
||||
fn infer_optional_type_expression(
|
||||
&mut self,
|
||||
expression: Option<&ast::Expr>,
|
||||
) -> Option<Type<'db>> {
|
||||
expression.map(|expr| self.infer_type_expression(expr))
|
||||
}
|
||||
|
||||
fn report_invalid_type_expression(
|
||||
&self,
|
||||
expression: &ast::Expr,
|
||||
|
@ -11541,38 +11559,20 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
typevar
|
||||
.default_ty(&db)
|
||||
.default_type(&db)
|
||||
.map(|ty| ty.display(&db).to_string()),
|
||||
default.map(std::borrow::ToOwned::to_owned)
|
||||
);
|
||||
};
|
||||
|
||||
check_typevar("T", "typing.TypeVar(\"T\")", None, None, None);
|
||||
check_typevar("U", "typing.TypeVar(\"U\", bound=A)", Some("A"), None, None);
|
||||
check_typevar(
|
||||
"V",
|
||||
"typing.TypeVar(\"V\", A, B)",
|
||||
None,
|
||||
Some(&["A", "B"]),
|
||||
None,
|
||||
);
|
||||
check_typevar(
|
||||
"W",
|
||||
"typing.TypeVar(\"W\", default=A)",
|
||||
None,
|
||||
None,
|
||||
Some("A"),
|
||||
);
|
||||
check_typevar(
|
||||
"X",
|
||||
"typing.TypeVar(\"X\", bound=A, default=A1)",
|
||||
Some("A"),
|
||||
None,
|
||||
Some("A1"),
|
||||
);
|
||||
check_typevar("T", "typing.TypeVar", None, None, None);
|
||||
check_typevar("U", "typing.TypeVar", Some("A"), None, None);
|
||||
check_typevar("V", "typing.TypeVar", None, Some(&["A", "B"]), None);
|
||||
check_typevar("W", "typing.TypeVar", None, None, Some("A"));
|
||||
check_typevar("X", "typing.TypeVar", Some("A"), None, Some("A1"));
|
||||
|
||||
// a typevar with less than two constraints is treated as unconstrained
|
||||
check_typevar("Y", "typing.TypeVar(\"Y\")", None, None, None);
|
||||
check_typevar("Y", "typing.TypeVar", None, None, None);
|
||||
}
|
||||
|
||||
/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing
|
||||
|
|
|
@ -1797,11 +1797,7 @@ mod tests {
|
|||
};
|
||||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// TODO resolution should not be deferred; we should see A, not A | B
|
||||
assert_eq!(
|
||||
a_annotated_ty.unwrap().display(&db).to_string(),
|
||||
"Unknown | A | B"
|
||||
);
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "A");
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T@f");
|
||||
}
|
||||
|
||||
|
|
|
@ -97,9 +97,12 @@ impl<'db> SubclassOfType<'db> {
|
|||
db,
|
||||
Name::new_static("T_all"),
|
||||
None,
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(
|
||||
KnownClass::Type.to_instance(db),
|
||||
)),
|
||||
Some(
|
||||
TypeVarBoundOrConstraints::UpperBound(
|
||||
KnownClass::Type.to_instance(db),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
variance,
|
||||
None,
|
||||
TypeVarKind::Pep695,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue