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:
Brent Westbrook 2025-08-26 09:49:08 -04:00 committed by GitHub
parent 911d5cc973
commit bc7274d148
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 287 additions and 37 deletions

View file

@ -166,9 +166,49 @@ bitflags! {
}
}
#[derive(Debug, is_macro::Is)]
#[derive(Clone, Copy, Debug, is_macro::Is)]
pub enum ScopeKind<'a> {
Class(&'a ast::StmtClassDef),
/// The implicit `__class__` scope surrounding a method which allows code in the
/// method to access `__class__` at runtime. The closure sits in between the class
/// scope and the function scope.
///
/// Parameter defaults in methods cannot access `__class__`:
///
/// ```pycon
/// >>> class Bar:
/// ... def method(self, x=__class__): ...
/// ...
/// Traceback (most recent call last):
/// File "<python-input-6>", line 1, in <module>
/// class Bar:
/// def method(self, x=__class__): ...
/// File "<python-input-6>", line 2, in Bar
/// def method(self, x=__class__): ...
/// ^^^^^^^^^
/// NameError: name '__class__' is not defined
/// ```
///
/// However, type parameters in methods *can* access `__class__`:
///
/// ```pycon
/// >>> class Foo:
/// ... def bar[T: __class__](): ...
/// ...
/// >>> Foo.bar.__type_params__[0].__bound__
/// <class '__main__.Foo'>
/// ```
///
/// Note that this is still not 100% accurate! At runtime, the implicit `__class__`
/// closure is only added if the name `super` (has to be a name -- `builtins.super`
/// and similar don't count!) or the name `__class__` is used in any method of the
/// class. However, accurately emulating that would be both complex and probably
/// quite expensive unless we moved to a double-traversal of each scope similar to
/// ty. It would also only matter in extreme and unlikely edge cases. So we ignore
/// that subtlety for now.
///
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>.
DunderClassCell,
Function(&'a ast::StmtFunctionDef),
Generator {
kind: GeneratorKind,