[pyupgrade] Apply UP008 only when the __class__ cell exists (#19424)

## Summary

Resolves #19357 

Skip UP008 diagnostic for `builtins.super(P, self)` calls when
`__class__` is not referenced locally, preventing incorrect fixes.

**Note:** I haven't found concrete information about which cases
`__class__` will be loaded into the scope. Let me know if anyone has
references, it would be useful to enhance the implementation. I did a
lot of tests to determine when `__class__` is loaded. Considered
sources:
1. [Python doc
super](https://docs.python.org/3/library/functions.html#super)
2. [Python doc classes](https://docs.python.org/3/tutorial/classes.html)
3. [pep-3135](https://peps.python.org/pep-3135/#specification)

As I understand it, Python will inject at runtime into local scope a
`__class__` variable if it detects references to `super` or `__class__`.
This allows calling `super()` and passing appropriate parameters.
However, the compiler doesn't do the same for `builtins.super`, so we
need to somehow introduce `__class__` into the local scope.

I figured out `__class__` will be in scope with valid value when two
conditions are met:
1. `super` or `__class__` names have been loaded within function scope
4. `__class__` is not overridden.

I think my solution isn't elegant, so I would be appreciate a detailed
review.

## Test Plan

Added 19 test cases, updated snapshots.

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
This commit is contained in:
Igor Drokin 2025-09-09 21:59:23 +03:00 committed by GitHub
parent d7524ea6d4
commit 54df73c9f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 620 additions and 41 deletions

View file

@ -141,3 +141,133 @@ class ExampleWithKeywords:
def method3(self):
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
# See: https://github.com/astral-sh/ruff/issues/19357
# Must be detected
class ParentD:
def f(self):
print("D")
class ChildD1(ParentD):
def f(self):
if False: __class__ # Python injects __class__ into scope
builtins.super(ChildD1, self).f()
class ChildD2(ParentD):
def f(self):
if False: super # Python injects __class__ into scope
builtins.super(ChildD2, self).f()
class ChildD3(ParentD):
def f(self):
builtins.super(ChildD3, self).f()
super # Python injects __class__ into scope
import builtins as builtins_alias
class ChildD4(ParentD):
def f(self):
builtins_alias.super(ChildD4, self).f()
super # Python injects __class__ into scope
class ChildD5(ParentD):
def f(self):
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD5, self).f()
class ChildD6(ParentD):
def f(self):
super: "Any"
__class__ # Python injects __class__ into scope
builtins.super(ChildD6, self).f()
class ChildD7(ParentD):
def f(self):
def x():
__class__ # Python injects __class__ into scope
builtins.super(ChildD7, self).f()
class ChildD8(ParentD):
def f(self):
def x():
super = 1
super # Python injects __class__ into scope
builtins.super(ChildD8, self).f()
class ChildD9(ParentD):
def f(self):
def x():
__class__ = 1
__class__ # Python injects __class__ into scope
builtins.super(ChildD9, self).f()
class ChildD10(ParentD):
def f(self):
def x():
__class__ = 1
super # Python injects __class__ into scope
builtins.super(ChildD10, self).f()
# Must be ignored
class ParentI:
def f(self):
print("I")
class ChildI1(ParentI):
def f(self):
builtins.super(ChildI1, self).f() # no __class__ in the local scope
class ChildI2(ParentI):
def b(self):
x = __class__
if False: super
def f(self):
self.b()
builtins.super(ChildI2, self).f() # no __class__ in the local scope
class ChildI3(ParentI):
def f(self):
if False: super
def x(_):
builtins.super(ChildI3, self).f() # no __class__ in the local scope
x(None)
class ChildI4(ParentI):
def f(self):
super: "str"
builtins.super(ChildI4, self).f() # no __class__ in the local scope
class ChildI5(ParentI):
def f(self):
super = 1
__class__ = 3
builtins.super(ChildI5, self).f() # no __class__ in the local scope
class ChildI6(ParentI):
def f(self):
__class__ = None
__class__
builtins.super(ChildI6, self).f() # no __class__ in the local scope
class ChildI7(ParentI):
def f(self):
__class__ = None
super
builtins.super(ChildI7, self).f()
class ChildI8(ParentI):
def f(self):
__class__: "Any"
super
builtins.super(ChildI8, self).f()
class ChildI9(ParentI):
def f(self):
class A:
def foo(self):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()

View file

@ -1,6 +1,8 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextSize};
use crate::checkers::ast::Checker;
@ -94,14 +96,22 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
};
// Find the enclosing function definition (if any).
let Some(Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
})) = parents.find(|stmt| stmt.is_function_def_stmt())
let Some(
func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef {
parameters: parent_parameters,
..
}),
) = parents.find(|stmt| stmt.is_function_def_stmt())
else {
return;
};
if is_builtins_super(checker.semantic(), call)
&& !has_local_dunder_class_var_ref(checker.semantic(), func_stmt)
{
return;
}
// Extract the name of the first argument to the enclosing function.
let Some(parent_arg) = parent_parameters.args.first() else {
return;
@ -193,3 +203,67 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
fn is_super_call_with_arguments(call: &ast::ExprCall, checker: &Checker) -> bool {
checker.semantic().match_builtin_expr(&call.func, "super") && !call.arguments.is_empty()
}
/// Returns `true` if the function contains load references to `__class__` or `super` without
/// local binding.
///
/// This indicates that the function relies on the implicit `__class__` cell variable created by
/// Python when `super()` is called without arguments, making it unsafe to remove `super()` parameters.
fn has_local_dunder_class_var_ref(semantic: &SemanticModel, func_stmt: &Stmt) -> bool {
if semantic.current_scope().has("__class__") {
return false;
}
let mut finder = ClassCellReferenceFinder::new();
finder.visit_stmt(func_stmt);
finder.found()
}
/// Returns `true` if the call is to the built-in `builtins.super` function.
fn is_builtins_super(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
semantic
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["builtins", "super"]))
}
/// A [`Visitor`] that searches for implicit reference to `__class__` cell,
/// excluding nested class definitions.
#[derive(Debug)]
struct ClassCellReferenceFinder {
has_class_cell: bool,
}
impl ClassCellReferenceFinder {
pub(crate) fn new() -> Self {
ClassCellReferenceFinder {
has_class_cell: false,
}
}
pub(crate) fn found(&self) -> bool {
self.has_class_cell
}
}
impl<'a> Visitor<'a> for ClassCellReferenceFinder {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::ClassDef(_) => {}
_ => {
if !self.has_class_cell {
walk_stmt(self, stmt);
}
}
}
}
fn visit_expr(&mut self, expr: &'a Expr) {
if expr.as_name_expr().is_some_and(|name| {
matches!(name.id.as_str(), "super" | "__class__") && name.ctx.is_load()
}) {
self.has_class_cell = true;
return;
}
walk_expr(self, expr);
}
}

View file

@ -146,25 +146,6 @@ help: Remove `super()` parameters
95 | # see: https://github.com/astral-sh/ruff/issues/18684
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@ -294,6 +275,8 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@ -301,4 +284,213 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
note: This is an unsafe fix and may change runtime behavior
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored
note: This is an unsafe fix and may change runtime behavior

View file

@ -139,24 +139,6 @@ help: Remove `super()` parameters
94 |
95 | # see: https://github.com/astral-sh/ruff/issues/18684
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:107:23
|
105 | class C:
106 | def f(self):
107 | builtins.super(C, self)
| ^^^^^^^^^
|
help: Remove `super()` parameters
104 |
105 | class C:
106 | def f(self):
- builtins.super(C, self)
107 + builtins.super()
108 |
109 |
110 | # see: https://github.com/astral-sh/ruff/issues/18533
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:113:14
|
@ -286,6 +268,8 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
142 | def method3(self):
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
help: Remove `super()` parameters
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
@ -293,3 +277,202 @@ help: Remove `super()` parameters
142 | def method3(self):
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
143 + super().some_method() # Should be fixed - no keywords
144 |
145 | # See: https://github.com/astral-sh/ruff/issues/19357
146 | # Must be detected
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:154:23
|
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
154 | builtins.super(ChildD1, self).f()
| ^^^^^^^^^^^^^^^
155 |
156 | class ChildD2(ParentD):
|
help: Remove `super()` parameters
151 | class ChildD1(ParentD):
152 | def f(self):
153 | if False: __class__ # Python injects __class__ into scope
- builtins.super(ChildD1, self).f()
154 + builtins.super().f()
155 |
156 | class ChildD2(ParentD):
157 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:159:23
|
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
159 | builtins.super(ChildD2, self).f()
| ^^^^^^^^^^^^^^^
160 |
161 | class ChildD3(ParentD):
|
help: Remove `super()` parameters
156 | class ChildD2(ParentD):
157 | def f(self):
158 | if False: super # Python injects __class__ into scope
- builtins.super(ChildD2, self).f()
159 + builtins.super().f()
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:163:23
|
161 | class ChildD3(ParentD):
162 | def f(self):
163 | builtins.super(ChildD3, self).f()
| ^^^^^^^^^^^^^^^
164 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
160 |
161 | class ChildD3(ParentD):
162 | def f(self):
- builtins.super(ChildD3, self).f()
163 + builtins.super().f()
164 | super # Python injects __class__ into scope
165 |
166 | import builtins as builtins_alias
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:169:29
|
167 | class ChildD4(ParentD):
168 | def f(self):
169 | builtins_alias.super(ChildD4, self).f()
| ^^^^^^^^^^^^^^^
170 | super # Python injects __class__ into scope
|
help: Remove `super()` parameters
166 | import builtins as builtins_alias
167 | class ChildD4(ParentD):
168 | def f(self):
- builtins_alias.super(ChildD4, self).f()
169 + builtins_alias.super().f()
170 | super # Python injects __class__ into scope
171 |
172 | class ChildD5(ParentD):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:176:23
|
174 | super = 1
175 | super # Python injects __class__ into scope
176 | builtins.super(ChildD5, self).f()
| ^^^^^^^^^^^^^^^
177 |
178 | class ChildD6(ParentD):
|
help: Remove `super()` parameters
173 | def f(self):
174 | super = 1
175 | super # Python injects __class__ into scope
- builtins.super(ChildD5, self).f()
176 + builtins.super().f()
177 |
178 | class ChildD6(ParentD):
179 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:182:23
|
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
182 | builtins.super(ChildD6, self).f()
| ^^^^^^^^^^^^^^^
183 |
184 | class ChildD7(ParentD):
|
help: Remove `super()` parameters
179 | def f(self):
180 | super: "Any"
181 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD6, self).f()
182 + builtins.super().f()
183 |
184 | class ChildD7(ParentD):
185 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:188:23
|
186 | def x():
187 | __class__ # Python injects __class__ into scope
188 | builtins.super(ChildD7, self).f()
| ^^^^^^^^^^^^^^^
189 |
190 | class ChildD8(ParentD):
|
help: Remove `super()` parameters
185 | def f(self):
186 | def x():
187 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD7, self).f()
188 + builtins.super().f()
189 |
190 | class ChildD8(ParentD):
191 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:195:23
|
193 | super = 1
194 | super # Python injects __class__ into scope
195 | builtins.super(ChildD8, self).f()
| ^^^^^^^^^^^^^^^
196 |
197 | class ChildD9(ParentD):
|
help: Remove `super()` parameters
192 | def x():
193 | super = 1
194 | super # Python injects __class__ into scope
- builtins.super(ChildD8, self).f()
195 + builtins.super().f()
196 |
197 | class ChildD9(ParentD):
198 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:202:23
|
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
202 | builtins.super(ChildD9, self).f()
| ^^^^^^^^^^^^^^^
203 |
204 | class ChildD10(ParentD):
|
help: Remove `super()` parameters
199 | def x():
200 | __class__ = 1
201 | __class__ # Python injects __class__ into scope
- builtins.super(ChildD9, self).f()
202 + builtins.super().f()
203 |
204 | class ChildD10(ParentD):
205 | def f(self):
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> UP008.py:209:23
|
207 | __class__ = 1
208 | super # Python injects __class__ into scope
209 | builtins.super(ChildD10, self).f()
| ^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
206 | def x():
207 | __class__ = 1
208 | super # Python injects __class__ into scope
- builtins.super(ChildD10, self).f()
209 + builtins.super().f()
210 |
211 |
212 | # Must be ignored