-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Diagnostic for generic classes that reference typevars in enclosing scope #20822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
97feecb
7f62ddd
cae5c63
1865d0d
5476cd1
2a6cb1e
5118548
940c191
bcd78c4
ba721fb
d90c207
ae0f00a
5965ffe
6ca9f93
23f81f3
10a1b54
e44582a
570b30d
c526090
40c48cc
8823fd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 | ||
|
||
``` |
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; | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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>> { | ||
|
@@ -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); | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
/// | ||
|
There was a problem hiding this comment.
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 parametersThere was a problem hiding this comment.
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