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

View file

@ -14,7 +14,7 @@ use crate::stdlib::{
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
};
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 use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
@ -1425,7 +1425,17 @@ impl<'db> ClassType<'db> {
class_stmt_node
.bases()
.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`.

View file

@ -867,7 +867,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let ast::StmtClassDef {
range: _,
name,
type_params: _,
type_params,
decorator_list,
arguments: _,
body: _,
@ -885,6 +885,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let maybe_known_class = file_to_module(self.db, body_scope.file(self.db))
.as_ref()
.and_then(|module| KnownClass::maybe_from_module(module, name.as_str()));
let class_ty = Type::Class(ClassType::new(
self.db,
name.id.clone(),
@ -895,17 +896,21 @@ impl<'db> TypeInferenceBuilder<'db> {
self.add_declaration_with_binding(class.into(), definition, class_ty, class_ty);
for keyword in class.keywords() {
self.infer_expression(&keyword.value);
}
// if there are type parameters, then the keywords and bases are within that scope
// 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
// TODO also defer stringified generic type parameters
if self.are_all_types_deferred() {
self.types.has_deferred = true;
} else {
for base in class.bases() {
self.infer_expression(base);
// Inference of bases deferred in stubs
// TODO also defer stringified generic type parameters
if self.are_all_types_deferred() {
self.types.has_deferred = true;
} else {
for base in class.bases() {
self.infer_expression(base);
}
}
}
}