mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:37 +00:00
[red-knot] Use the right scope when considering class bases (#13766)
Summary --------- PEP 695 Generics introduce a scope inside a class statement's arguments and keywords. ``` class C[T](A[T]): # the T in A[T] is not from the global scope but from a type-param-specfic scope ... ``` When doing inference on the class bases, we currently have been doing base class expression lookups in the global scope. Not an issue without generics (since a scope is only created when generics are present). This change instead makes sure to stop the global scope inference from going into expressions within this sub-scope. Since there is a separate scope, `check_file` and friends will trigger inference on these expressions still. Another change as a part of this is making sure that `ClassType` looks up its bases in the right scope. Test Plan ---------- `cargo test --package red_knot_python_semantic generics` will run the markdown test that previously would panic due to scope lookup issues --------- Co-authored-by: Micha Reiser <micha@reiser.io> Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
e2a30b71f4
commit
3d0bdb426a
4 changed files with 89 additions and 14 deletions
57
crates/red_knot_python_semantic/resources/mdtest/generics.md
Normal file
57
crates/red_knot_python_semantic/resources/mdtest/generics.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# PEP 695 Generics
|
||||||
|
|
||||||
|
## Class Declarations
|
||||||
|
|
||||||
|
Basic PEP 695 generics
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyBox[T]:
|
||||||
|
data: T
|
||||||
|
box_model_number = 695
|
||||||
|
def __init__(self, data: T):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
# TODO not error (should be subscriptable)
|
||||||
|
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
|
||||||
|
# TODO error differently (str and int don't unify)
|
||||||
|
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
|
||||||
|
# TODO reveal int
|
||||||
|
reveal_type(box.data) # revealed: @Todo
|
||||||
|
|
||||||
|
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subclassing
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyBox[T]:
|
||||||
|
data: T
|
||||||
|
|
||||||
|
def __init__(self, data: T):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
# TODO not error on the subscripting
|
||||||
|
class MySecureBox[T](MyBox[T]): # error: [non-subscriptable]
|
||||||
|
pass
|
||||||
|
|
||||||
|
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||||
|
reveal_type(secure_box) # revealed: MySecureBox
|
||||||
|
# TODO reveal int
|
||||||
|
reveal_type(secure_box.data) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cyclical class definition
|
||||||
|
|
||||||
|
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
|
||||||
|
|
||||||
|
This should hold true even with generics at play.
|
||||||
|
|
||||||
|
```py path=a.pyi
|
||||||
|
class Seq[T]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO not error on the subscripting
|
||||||
|
class S[T](Seq[S]): # error: [non-subscriptable]
|
||||||
|
pass
|
||||||
|
reveal_type(S) # revealed: Literal[S]
|
||||||
|
```
|
|
@ -34,7 +34,10 @@ pub(crate) struct AstIds {
|
||||||
|
|
||||||
impl AstIds {
|
impl AstIds {
|
||||||
fn expression_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedExpressionId {
|
fn expression_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedExpressionId {
|
||||||
self.expressions_map[&key.into()]
|
let key = &key.into();
|
||||||
|
*self.expressions_map.get(key).unwrap_or_else(|| {
|
||||||
|
panic!("Could not find expression ID for {key:?}");
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedUseId {
|
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedUseId {
|
||||||
|
|
|
@ -14,7 +14,7 @@ use crate::stdlib::{
|
||||||
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
||||||
};
|
};
|
||||||
use crate::types::narrow::narrowing_constraint;
|
use crate::types::narrow::narrowing_constraint;
|
||||||
use crate::{Db, FxOrderSet, Module};
|
use crate::{Db, FxOrderSet, HasTy, Module, SemanticModel};
|
||||||
|
|
||||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||||
|
@ -1425,7 +1425,17 @@ impl<'db> ClassType<'db> {
|
||||||
class_stmt_node
|
class_stmt_node
|
||||||
.bases()
|
.bases()
|
||||||
.iter()
|
.iter()
|
||||||
.map(move |base_expr| definition_expression_ty(db, definition, base_expr))
|
.map(move |base_expr: &ast::Expr| {
|
||||||
|
if class_stmt_node.type_params.is_some() {
|
||||||
|
// when we have a specialized scope, we'll look up the inference
|
||||||
|
// within that scope
|
||||||
|
let model: SemanticModel<'db> = SemanticModel::new(db, definition.file(db));
|
||||||
|
base_expr.ty(&model)
|
||||||
|
} else {
|
||||||
|
// Otherwise, we can do the lookup based on the definition scope
|
||||||
|
definition_expression_ty(db, definition, base_expr)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the class member of this class named `name`.
|
/// Returns the class member of this class named `name`.
|
||||||
|
|
|
@ -867,7 +867,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let ast::StmtClassDef {
|
let ast::StmtClassDef {
|
||||||
range: _,
|
range: _,
|
||||||
name,
|
name,
|
||||||
type_params: _,
|
type_params,
|
||||||
decorator_list,
|
decorator_list,
|
||||||
arguments: _,
|
arguments: _,
|
||||||
body: _,
|
body: _,
|
||||||
|
@ -885,6 +885,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let maybe_known_class = file_to_module(self.db, body_scope.file(self.db))
|
let maybe_known_class = file_to_module(self.db, body_scope.file(self.db))
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|module| KnownClass::maybe_from_module(module, name.as_str()));
|
.and_then(|module| KnownClass::maybe_from_module(module, name.as_str()));
|
||||||
|
|
||||||
let class_ty = Type::Class(ClassType::new(
|
let class_ty = Type::Class(ClassType::new(
|
||||||
self.db,
|
self.db,
|
||||||
name.id.clone(),
|
name.id.clone(),
|
||||||
|
@ -895,17 +896,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
self.add_declaration_with_binding(class.into(), definition, class_ty, class_ty);
|
self.add_declaration_with_binding(class.into(), definition, class_ty, class_ty);
|
||||||
|
|
||||||
for keyword in class.keywords() {
|
// if there are type parameters, then the keywords and bases are within that scope
|
||||||
self.infer_expression(&keyword.value);
|
// and we don't need to run inference here
|
||||||
}
|
if type_params.is_none() {
|
||||||
|
for keyword in class.keywords() {
|
||||||
|
self.infer_expression(&keyword.value);
|
||||||
|
}
|
||||||
|
|
||||||
// Inference of bases deferred in stubs
|
// Inference of bases deferred in stubs
|
||||||
// TODO also defer stringified generic type parameters
|
// TODO also defer stringified generic type parameters
|
||||||
if self.are_all_types_deferred() {
|
if self.are_all_types_deferred() {
|
||||||
self.types.has_deferred = true;
|
self.types.has_deferred = true;
|
||||||
} else {
|
} else {
|
||||||
for base in class.bases() {
|
for base in class.bases() {
|
||||||
self.infer_expression(base);
|
self.infer_expression(base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue