Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ reveal_type(generic_context(InheritedGenericPartiallySpecialized))
reveal_type(generic_context(InheritedGenericFullySpecialized))
```

In a nested class, references to typevars in an enclosing class are not allowed, but if they are
present, they are not included in the class's generic context.

```py
class OuterClass(Generic[T]):
# error: [invalid-generic-class] "Generic class `InnerClass` must not reference type variables bound in an enclosing scope"
class InnerClass(list[T]): ...
# revealed: None
reveal_type(generic_context(InnerClass))

def method(self):
# error: [invalid-generic-class] "Generic class `InnerClassInMethod` must not reference type variables bound in an enclosing scope"
class InnerClassInMethod(list[T]): ...
# revealed: None
reveal_type(generic_context(InnerClassInMethod))

# revealed: tuple[T@OuterClass]
reveal_type(generic_context(OuterClass))
```

If you don't specialize a generic base class, we use the default specialization, which maps each
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
the inheriting class generic.
Expand Down
12 changes: 8 additions & 4 deletions crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,27 +260,31 @@ class C[T]:

### Generic class within generic function

<!-- snapshot-diagnostics -->

```py
from typing import Iterable

def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class]
class Bad1[T]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class]
class Bad2(Iterable[T]): ...
```

### Generic class within generic class

<!-- snapshot-diagnostics -->

```py
from typing import Iterable

class C[T]:
class Ok1[S]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class]
class Bad1[T]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class]
class Bad2(Iterable[T]): ...
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic class within generic function
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Iterable
2 |
3 | def f[T](x: T, y: T) -> None:
4 | class Ok[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
```

# Diagnostics

```
error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope
--> src/mdtest_snippet.py:3:5
|
1 | from typing import Iterable
2 |
3 | def f[T](x: T, y: T) -> None:
| ------------------------ Type variable `T` is bound in this enclosing scope
4 | class Ok[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
| ^^^^ `T` referenced in class definition here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, this seems like a pre-existing bug in ClassLiteral::header_span() that it doesn't cover the type parameters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I saw that too

7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
|
info: rule `invalid-generic-class` is enabled by default

```

```
error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope
--> src/mdtest_snippet.py:3:5
|
1 | from typing import Iterable
2 |
3 | def f[T](x: T, y: T) -> None:
| ------------------------ Type variable `T` is bound in this enclosing scope
4 | class Ok[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
| ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here
|
info: rule `invalid-generic-class` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: scoping.md - Scoping rules for type variables - Nested formal typevars must be distinct - Generic class within generic class
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Iterable
2 |
3 | class C[T]:
4 | class Ok1[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
```

# Diagnostics

```
error[invalid-generic-class]: Generic class `Bad1` must not reference type variables bound in an enclosing scope
--> src/mdtest_snippet.py:3:7
|
1 | from typing import Iterable
2 |
3 | class C[T]:
| - Type variable `T` is bound in this enclosing scope
4 | class Ok1[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
| ^^^^ `T` referenced in class definition here
7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
|
info: rule `invalid-generic-class` is enabled by default

```

```
error[invalid-generic-class]: Generic class `Bad2` must not reference type variables bound in an enclosing scope
--> src/mdtest_snippet.py:3:7
|
1 | from typing import Iterable
2 |
3 | class C[T]:
| - Type variable `T` is bound in this enclosing scope
4 | class Ok1[S]: ...
5 | # error: [invalid-generic-class]
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class]
8 | class Bad2(Iterable[T]): ...
| ^^^^^^^^^^^^^^^^^ `T` referenced in class definition here
|
info: rule `invalid-generic-class` is enabled by default

```
8 changes: 6 additions & 2 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8184,12 +8184,16 @@ impl<'db> From<Definition<'db>> for BindingContext<'db> {
}

impl<'db> BindingContext<'db> {
fn name(self, db: &'db dyn Db) -> Option<String> {
pub(crate) fn definition(self) -> Option<Definition<'db>> {
match self {
BindingContext::Definition(definition) => definition.name(db),
BindingContext::Definition(definition) => Some(definition),
BindingContext::Synthetic => None,
}
}

fn name(self, db: &'db dyn Db) -> Option<String> {
self.definition().and_then(|definition| definition.name(db))
}
}

/// A type variable that has been bound to a generic context, and which can be specialized to a
Expand Down
75 changes: 70 additions & 5 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::sync::{LazyLock, Mutex};

use super::TypeVarVariance;
Expand All @@ -19,11 +20,14 @@ use crate::types::context::InferContext;
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
use crate::types::enums::enum_metadata;
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization, walk_specialization};
use crate::types::generics::{
GenericContext, Specialization, walk_generic_context, walk_specialization,
};
use crate::types::infer::nearest_enclosing_class;
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::typed_dict::typed_dict_params_from_class_def;
use crate::types::visitor::{NonAtomicType, TypeKind, TypeVisitor, walk_non_atomic_type};
use crate::types::{
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
Expand All @@ -33,7 +37,7 @@ use crate::types::{
declaration_type, determine_upper_bound, infer_definition_types,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
module_resolver::file_to_module,
place::{
Boundness, LookupError, LookupResult, Place, PlaceAndQualifiers, class_symbol,
Expand Down Expand Up @@ -1380,7 +1384,7 @@ impl get_size2::GetSize for ClassLiteral<'_> {}

#[expect(clippy::ref_option)]
#[allow(clippy::trivially_copy_pass_by_ref)]
fn pep695_generic_context_cycle_recover<'db>(
fn generic_context_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Option<GenericContext<'db>>,
_count: u32,
Expand All @@ -1389,7 +1393,7 @@ fn pep695_generic_context_cycle_recover<'db>(
salsa::CycleRecoveryAction::Iterate
}

fn pep695_generic_context_cycle_initial<'db>(
fn generic_context_cycle_initial<'db>(
_db: &'db dyn Db,
_self: ClassLiteral<'db>,
) -> Option<GenericContext<'db>> {
Expand Down Expand Up @@ -1429,7 +1433,11 @@ impl<'db> ClassLiteral<'db> {
self.pep695_generic_context(db).is_some()
}

#[salsa::tracked(cycle_fn=pep695_generic_context_cycle_recover, cycle_initial=pep695_generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(
cycle_fn=generic_context_cycle_recover,
cycle_initial=generic_context_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
)]
pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
let scope = self.body_scope(db);
let file = scope.file(db);
Expand All @@ -1452,19 +1460,76 @@ impl<'db> ClassLiteral<'db> {
})
}

#[salsa::tracked(
cycle_fn=generic_context_cycle_recover,
cycle_initial=generic_context_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
)]
Comment on lines +1463 to +1467
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the memory regression. Would it be possible to only cache the cases where we have a type var? Or is this already what this is doing :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can without walking the base class in the same way

pub(crate) fn inherited_legacy_generic_context(
self,
db: &'db dyn Db,
) -> Option<GenericContext<'db>> {
GenericContext::from_base_classes(
db,
self.definition(db),
self.explicit_bases(db)
.iter()
.copied()
.filter(|ty| matches!(ty, Type::GenericAlias(_))),
)
}

/// Returns all of the typevars that are referenced in this class's definition. This includes
/// any typevars bound in its generic context, as well as any typevars mentioned in its base
/// class list. (This is used to ensure that classes do not bind or reference typevars from
/// enclosing generic contexts.)
pub(crate) fn typevars_referenced_in_definition(
self,
db: &'db dyn Db,
) -> FxIndexSet<BoundTypeVarInstance<'db>> {
#[derive(Default)]
struct CollectTypeVars<'db> {
typevars: RefCell<FxIndexSet<BoundTypeVarInstance<'db>>>,
seen_types: RefCell<FxIndexSet<NonAtomicType<'db>>>,
}

impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> {
fn should_visit_lazy_type_attributes(&self) -> bool {
false
}

fn visit_bound_type_var_type(
&self,
_db: &'db dyn Db,
bound_typevar: BoundTypeVarInstance<'db>,
) {
self.typevars.borrow_mut().insert(bound_typevar);
}

fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
match TypeKind::from(ty) {
TypeKind::Atomic => {}
TypeKind::NonAtomic(non_atomic_type) => {
if !self.seen_types.borrow_mut().insert(non_atomic_type) {
// If we have already seen this type, we can skip it.
return;
}
walk_non_atomic_type(db, non_atomic_type, self);
}
}
}
}

let visitor = CollectTypeVars::default();
if let Some(generic_context) = self.generic_context(db) {
walk_generic_context(db, generic_context, &visitor);
}
for base in self.explicit_bases(db) {
visitor.visit_type(db, *base);
}
visitor.typevars.into_inner()
}

/// Returns the generic context that should be inherited by any constructor methods of this
/// class.
///
Expand Down
Loading
Loading