Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
class Bad1[T]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
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] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
class Bad1[T]: ...
# TODO: error for reuse of typevar
# error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
class Bad2(Iterable[T]): ...
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
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] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
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:1
|
1 | from typing import Iterable
2 |
3 | / def f[T](x: T, y: T) -> None:
4 | | class Ok[S]: ...
5 | | # error: [invalid-generic-class] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | | class Bad1[T]: ...
| | ^^^^
7 | | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
8 | | class Bad2(Iterable[T]): ...
| |________________________________^ Type variable `T` is bound in this enclosing scope
|
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:1
|
1 | from typing import Iterable
2 |
3 | / def f[T](x: T, y: T) -> None:
4 | | class Ok[S]: ...
5 | | # error: [invalid-generic-class] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | | class Bad1[T]: ...
7 | | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
8 | | class Bad2(Iterable[T]): ...
| |___________^^^^_________________^ Type variable `T` is bound in this enclosing scope
|
info: rule `invalid-generic-class` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
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] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | class Bad1[T]: ...
7 | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
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:1
|
1 | from typing import Iterable
2 |
3 | / class C[T]:
4 | | class Ok1[S]: ...
5 | | # error: [invalid-generic-class] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | | class Bad1[T]: ...
| | ^^^^
7 | | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
8 | | class Bad2(Iterable[T]): ...
| |________________________________^ Type variable `T` is bound in this enclosing scope
|
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:1
|
1 | from typing import Iterable
2 |
3 | / class C[T]:
4 | | class Ok1[S]: ...
5 | | # error: [invalid-generic-class] "Generic class `Bad1` must not reference type variables bound in an enclosing scope"
6 | | class Bad1[T]: ...
7 | | # error: [invalid-generic-class] "Generic class `Bad2` must not reference type variables bound in an enclosing scope"
8 | | class Bad2(Iterable[T]): ...
| |___________^^^^_________________^ Type variable `T` is bound in this enclosing scope
|
info: rule `invalid-generic-class` is enabled by default

```
60 changes: 59 additions & 1 deletion 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 @@ -24,6 +25,7 @@ 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 +35,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 @@ -1458,13 +1460,69 @@ impl<'db> ClassLiteral<'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,
) -> FxOrderSet<BoundTypeVarInstance<'db>> {
struct CollectTypeVars<'db> {
typevars: RefCell<FxOrderSet<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 {
typevars: RefCell::new(FxOrderSet::default()),
seen_types: RefCell::new(FxIndexSet::default()),
};
if let Some(generic_context) = self.generic_context(db) {
for bound_typevar in generic_context.variables(db) {
visitor.visit_bound_type_var_type(db, bound_typevar);
}
}
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
36 changes: 30 additions & 6 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType,
MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping, TypeRelation,
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType,
binding_type, declaration_type,
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassLiteral,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext,
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
TypeVarVariance, UnionType, binding_type, declaration_type,
};
use crate::{Db, FxOrderMap, FxOrderSet};

/// Returns an iterator of any generic context introduced by the given scope or any enclosing
/// scope.
fn enclosing_generic_contexts<'db>(
pub(crate) fn enclosing_generic_contexts<'db>(
db: &'db dyn Db,
index: &SemanticIndex<'db>,
scope: FileScopeId,
Expand Down Expand Up @@ -317,15 +317,30 @@ impl<'db> GenericContext<'db> {
/// list.
pub(crate) fn from_base_classes(
db: &'db dyn Db,
definition: impl FnOnce() -> Definition<'db>,
Copy link
Member Author

Choose a reason for hiding this comment

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

See the comment below; this is passed in as a callback so that we don't have to query the class's definition unless it actually references legacy typevars in its base class list. Passing the definition in directly was causing some of the salsa query dependency tracking tests to fail.

Copy link
Member

Choose a reason for hiding this comment

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

That's probably worth an inline comment as it's non-obvious for future readers.

However, I do think what you did here is cheating the test rather than fixing the underlying issue. The problem is that calling definition adds a dependency on the ast node. The way you cheated the test is by early-returning in the case we're testing, a class with no type vars! But the same problem still exists for classes with type variables.

The more proper fix here is probably to make whatever calls from_base_classes to be behind a salsa query (at least, in positions where we can reach this call across file boundaries).

Copy link
Member Author

Choose a reason for hiding this comment

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

The more proper fix here is probably to make whatever calls from_base_classes to be behind a salsa query (at least, in positions where we can reach this call across file boundaries).

That was an easier real fix than I expected! Done

bases: impl Iterator<Item = Type<'db>>,
) -> Option<Self> {
let mut variables = FxOrderSet::default();
for base in bases {
base.find_legacy_typevars(db, None, &mut variables);
}

// If there are no legacy typevars mentioned in the base class list, we can return early.
if variables.is_empty() {
return None;
}

// If there are legacy typevars, filter out the ones that are not bound by this class. (We
// do this as a post-processing step, instead of by passing in a parameter to
// `find_legacy_typevars`, since there are very many classes that do not reference legacy
// typevars at all, and this avoids adding a salsa dependency on the class `Definition` in
// those cases.)
let binding_context = BindingContext::Definition(definition());
variables.retain(|bound_typevar| bound_typevar.binding_context(db) == binding_context);
if variables.is_empty() {
return None;
}

Some(Self::from_typevar_instances(db, variables))
}

Expand Down Expand Up @@ -413,6 +428,15 @@ impl<'db> GenericContext<'db> {
.all(|bound_typevar| other_variables.contains_key(&bound_typevar))
}

pub(crate) fn binds_named_typevar(
self,
db: &'db dyn Db,
name: &'db ast::name::Name,
) -> Option<BoundTypeVarInstance<'db>> {
self.variables(db)
.find(|self_bound_typevar| self_bound_typevar.typevar(db).name(db) == name)
}

pub(crate) fn binds_typevar(
self,
db: &'db dyn Db,
Expand Down
Loading
Loading