mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
Add a ScopeKind
for the __class__
cell (#20048)
Summary -- This PR aims to resolve (or help to resolve) #18442 and #19357 by encoding the CPython semantics around the `__class__` cell in our semantic model. Namely, > `__class__` is an implicit closure reference created by the compiler if any methods in a class body refer to either `__class__` or super. from the Python [docs](https://docs.python.org/3/reference/datamodel.html#creating-the-class-object). As noted in the variant docs by @AlexWaygood, we don't fully model this behavior, opting always to create the `__class__` cell binding in a new `ScopeKind::DunderClassCell` around each method definition, without checking if any method in the class body actually refers to `__class__` or `super`. As such, this PR fixes #18442 but not #19357. Test Plan -- Existing tests, plus the tests from #19783, which now pass without any rule-specific code. Note that we opted not to alter the behavior of F841 here because flagging `__class__` in these cases still seems helpful. See the discussion in https://github.com/astral-sh/ruff/pull/20048#discussion_r2296252395 and in the test comments for more information. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Mikko Leppänen <mleppan23@gmail.com>
This commit is contained in:
parent
911d5cc973
commit
bc7274d148
11 changed files with 287 additions and 37 deletions
|
@ -404,22 +404,11 @@ impl<'a> SemanticModel<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut seen_function = false;
|
||||
let mut import_starred = false;
|
||||
let mut class_variables_visible = true;
|
||||
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
|
||||
let scope = &self.scopes[scope_id];
|
||||
if scope.kind.is_class() {
|
||||
// Allow usages of `__class__` within methods, e.g.:
|
||||
//
|
||||
// ```python
|
||||
// class Foo:
|
||||
// def __init__(self):
|
||||
// print(__class__)
|
||||
// ```
|
||||
if seen_function && matches!(name.id.as_str(), "__class__") {
|
||||
return ReadResult::ImplicitGlobal;
|
||||
}
|
||||
// Do not allow usages of class symbols unless it is the immediate parent
|
||||
// (excluding type scopes), e.g.:
|
||||
//
|
||||
|
@ -442,7 +431,13 @@ impl<'a> SemanticModel<'a> {
|
|||
// Allow class variables to be visible for an additional scope level
|
||||
// when a type scope is seen — this covers the type scope present between
|
||||
// function and class definitions and their parent class scope.
|
||||
class_variables_visible = scope.kind.is_type() && index == 0;
|
||||
//
|
||||
// Also allow an additional level beyond that to cover the implicit
|
||||
// `__class__` closure created around methods and enclosing the type scope.
|
||||
class_variables_visible = matches!(
|
||||
(scope.kind, index),
|
||||
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
|
||||
);
|
||||
|
||||
if let Some(binding_id) = scope.get(name.id.as_str()) {
|
||||
// Mark the binding as used.
|
||||
|
@ -614,7 +609,6 @@ impl<'a> SemanticModel<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
seen_function |= scope.kind.is_function();
|
||||
import_starred = import_starred || scope.uses_star_imports();
|
||||
}
|
||||
|
||||
|
@ -658,21 +652,19 @@ impl<'a> SemanticModel<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut seen_function = false;
|
||||
let mut class_variables_visible = true;
|
||||
for (index, scope_id) in self.scopes.ancestor_ids(scope_id).enumerate() {
|
||||
let scope = &self.scopes[scope_id];
|
||||
if scope.kind.is_class() {
|
||||
if seen_function && matches!(symbol, "__class__") {
|
||||
return None;
|
||||
}
|
||||
if !class_variables_visible {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
class_variables_visible = scope.kind.is_type() && index == 0;
|
||||
seen_function |= scope.kind.is_function();
|
||||
class_variables_visible = matches!(
|
||||
(scope.kind, index),
|
||||
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
|
||||
);
|
||||
|
||||
if let Some(binding_id) = scope.get(symbol) {
|
||||
match self.bindings[binding_id].kind {
|
||||
|
@ -786,15 +778,15 @@ impl<'a> SemanticModel<'a> {
|
|||
}
|
||||
|
||||
if scope.kind.is_class() {
|
||||
if seen_function && matches!(symbol, "__class__") {
|
||||
return None;
|
||||
}
|
||||
if !class_variables_visible {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
class_variables_visible = scope.kind.is_type() && index == 0;
|
||||
class_variables_visible = matches!(
|
||||
(scope.kind, index),
|
||||
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
|
||||
);
|
||||
seen_function |= scope.kind.is_function();
|
||||
|
||||
if let Some(binding_id) = scope.get(symbol) {
|
||||
|
@ -1353,11 +1345,12 @@ impl<'a> SemanticModel<'a> {
|
|||
self.scopes[scope_id].parent
|
||||
}
|
||||
|
||||
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`], if any.
|
||||
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`] or
|
||||
/// [`ScopeKind::DunderClassCell`], if any.
|
||||
pub fn first_non_type_parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> {
|
||||
let mut current_scope = scope;
|
||||
while let Some(parent) = self.parent_scope(current_scope) {
|
||||
if parent.kind.is_type() {
|
||||
if matches!(parent.kind, ScopeKind::Type | ScopeKind::DunderClassCell) {
|
||||
current_scope = parent;
|
||||
} else {
|
||||
return Some(parent);
|
||||
|
@ -1366,11 +1359,15 @@ impl<'a> SemanticModel<'a> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`], if any.
|
||||
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`] or
|
||||
/// [`ScopeKind::DunderClassCell`], if any.
|
||||
pub fn first_non_type_parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
|
||||
let mut current_scope_id = scope_id;
|
||||
while let Some(parent_id) = self.parent_scope_id(current_scope_id) {
|
||||
if self.scopes[parent_id].kind.is_type() {
|
||||
if matches!(
|
||||
self.scopes[parent_id].kind,
|
||||
ScopeKind::Type | ScopeKind::DunderClassCell
|
||||
) {
|
||||
current_scope_id = parent_id;
|
||||
} else {
|
||||
return Some(parent_id);
|
||||
|
@ -2649,16 +2646,16 @@ pub enum ReadResult {
|
|||
/// The `x` in `print(x)` is resolved to the binding of `x` in `x = 1`.
|
||||
Resolved(BindingId),
|
||||
|
||||
/// The read reference is resolved to a context-specific, implicit global (e.g., `__class__`
|
||||
/// The read reference is resolved to a context-specific, implicit global (e.g., `__qualname__`
|
||||
/// within a class scope).
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// class C:
|
||||
/// print(__class__)
|
||||
/// print(__qualname__)
|
||||
/// ```
|
||||
///
|
||||
/// The `__class__` in `print(__class__)` is resolved to the implicit global `__class__`.
|
||||
/// The `__qualname__` in `print(__qualname__)` is resolved to the implicit global `__qualname__`.
|
||||
ImplicitGlobal,
|
||||
|
||||
/// The read reference is unresolved, but at least one of the containing scopes contains a
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue