[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:
Raphael Gaschignard 2024-10-18 08:29:46 +10:00 committed by GitHub
parent e2a30b71f4
commit 3d0bdb426a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 89 additions and 14 deletions

View 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]
```

View file

@ -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 {

View file

@ -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`.

View file

@ -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);
}
} }
} }
} }