Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,133 @@ def method2(self):

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()
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Comment thread
ntBre marked this conversation as resolved.
/// 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 {
Comment thread
ntBre marked this conversation as resolved.
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"]))
}
Comment thread
ntBre marked this conversation as resolved.

/// 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);
}
}
Loading
Loading