-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[red-knot] Use the right scope when considering class bases #13766
base: main
Are you sure you want to change the base?
Changes from 1 commit
d57ab6c
a1e430a
41fee40
46cc50b
8601fc8
1b9e355
42728f8
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,41 @@ | ||
# 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 | ||
``` |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -34,7 +34,13 @@ pub(crate) struct AstIds { | |||||||||||||||||||||||
|
||||||||||||||||||||||||
impl AstIds { | ||||||||||||||||||||||||
fn expression_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedExpressionId { | ||||||||||||||||||||||||
self.expressions_map[&key.into()] | ||||||||||||||||||||||||
let key = &key.into(); | ||||||||||||||||||||||||
match self.expressions_map.get(key) { | ||||||||||||||||||||||||
Some(result) => *result, | ||||||||||||||||||||||||
None => { | ||||||||||||||||||||||||
panic!("Could not find expression ID for {key:?}"); | ||||||||||||||||||||||||
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. This is very helpful because debug output on I put this in under the idea that this doesn't cost much but I might be wrong |
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
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 like this. You could do
Suggested change
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. Went with this, thanks for the suggestion! 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. Great, went with that |
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedUseId { | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
use infer::TypeInferenceBuilder; | ||
use infer::{TypeInference, TypeInferenceBuilder}; | ||
use ruff_db::files::File; | ||
use ruff_python_ast as ast; | ||
|
||
use crate::module_resolver::file_to_module; | ||
use crate::semantic_index::ast_ids::HasScopedAstId; | ||
use crate::semantic_index::definition::{Definition, DefinitionKind}; | ||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId}; | ||
use crate::semantic_index::symbol::{NodeWithScopeRef, ScopeId, ScopedSymbolId}; | ||
use crate::semantic_index::{ | ||
global_scope, semantic_index, symbol_table, use_def_map, BindingWithConstraints, | ||
BindingWithConstraintsIterator, DeclarationsIterator, | ||
|
@@ -1416,10 +1416,31 @@ impl<'db> ClassType<'db> { | |
let DefinitionKind::Class(class_stmt_node) = definition.kind(db) else { | ||
panic!("Class type definition must have DefinitionKind::Class"); | ||
}; | ||
class_stmt_node | ||
.bases() | ||
.iter() | ||
.map(move |base_expr| definition_expression_ty(db, definition, base_expr)) | ||
|
||
let has_type_params = class_stmt_node.type_params.is_some(); | ||
|
||
let type_scope_info: Option<(ScopeId<'db>, &TypeInference<'db>)> = if has_type_params { | ||
let file = definition.file(db); | ||
let index = semantic_index(db, file); | ||
// we need to use the type param'd scope | ||
let type_param_scope = index | ||
.node_scope(NodeWithScopeRef::ClassTypeParameters(class_stmt_node)) | ||
.to_scope_id(db, file); | ||
Some((type_param_scope, infer_scope_types(db, type_param_scope))) | ||
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. It's unclear to me what the cost of the Would it make sense to add 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 would expect that we could reuse the However, we probably don't want to call I'm not quiet sure but maybe 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 use We could mark the class bases as a "standalone expression" (or set of them) so we can query for their types directly, but I don't think this is worth the extra Salsa tracked structs. The "type params scope" is automatically generated and implicit and contains nothing but definitions for type parameters, and the class bases/keywords. There isn't "all types in that scope" to be worried about, because there are no other types in that scope. So I think 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.
We aren't in 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 think perhaps the simpler way to get the type params scope is that it must always be the parent scope of the body scope (for a class with type params.) We should be able to add a I'm also not opposed to adding 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. You can also use 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.
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. Ended up storing the bases scope in |
||
} else { | ||
None | ||
}; | ||
|
||
let mapper = move |base_expr: &ast::Expr| { | ||
if has_type_params { | ||
// Safety: we calculated the inference if has_type_params | ||
let (type_param_scope, inferences) = type_scope_info.unwrap(); | ||
inferences.expression_ty(base_expr.scoped_ast_id(db, type_param_scope)) | ||
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. Is there a reason to use 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. Hmm... your suggestion is cleaner and this code isn't big, but I dislike the hiding of the "the branching is due to type parameter-ness". I believe my concerns can be resolved with a more well thought out variable name for the scope info. Will ruminate on it overnight 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. Changed this up to |
||
} else { | ||
definition_expression_ty(db, definition, base_expr) | ||
} | ||
}; | ||
class_stmt_node.bases().iter().map(mapper) | ||
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. the mapper is pulled out here because if we build up separate closures in different |
||
} | ||
|
||
/// Returns the class member of this class named `name`. | ||
|
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.
I would like to also add a test that in a stub file (
pyi
) we can have a generic class that refers to itself in its own bases. We have a similar test already for non-generic classes.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.
Added