diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py index 50c4e45f57b5f..e1096d53f2d9a 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py @@ -396,7 +396,6 @@ def f(self): class LambdaMethod(BaseClass): - # TODO(charlie): class-body lambda rewrite is still missed. f = lambda self: super(LambdaMethod, self).f() # can use super() diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs index 813276376d6c2..c8cfbe8c7afd4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -2,7 +2,7 @@ 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::{ScopeKind, SemanticModel}; +use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; @@ -75,12 +75,18 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall if !is_super_call_with_arguments(call, checker) { return; } - let scope = checker.semantic().current_scope(); - - // Check: are we in a Function scope? - if !scope.kind.is_function() { + // Check: are we in a callable scope (function or lambda)? + let callable_scope = match &checker.semantic().current_scope().kind { + ScopeKind::Function(_) | ScopeKind::Lambda(_) => Some(checker.semantic().current_scope()), + ScopeKind::DunderClassCell => checker + .semantic() + .first_non_type_parent_scope(checker.semantic().current_scope()) + .filter(|scope| matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_))), + _ => None, + }; + let Some(callable_scope) = callable_scope else { return; - } + }; let mut parents = checker.semantic().current_statements(); // For a `super` invocation to be unnecessary, the first argument needs to match @@ -90,28 +96,53 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall return; }; - // Find the enclosing function definition (if any). - let Some( - func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef { - parameters: parent_parameters, + // Find the enclosing callable and extract the name of its first parameter. + let (parent_arg_name, has_local_dunder_class_var_ref) = match &callable_scope.kind { + ScopeKind::Function(_) => { + let Some( + func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef { + parameters: parent_parameters, + .. + }), + ) = parents.find(|stmt| stmt.is_function_def_stmt()) + else { + return; + }; + + let Some(parent_arg) = parent_parameters.args.first() else { + return; + }; + + ( + parent_arg.name().as_str(), + has_local_dunder_class_var_ref(callable_scope, |finder| { + finder.visit_stmt(func_stmt); + }), + ) + } + ScopeKind::Lambda(ast::ExprLambda { + parameters: Some(parent_parameters), + body, .. - }), - ) = parents.find(|stmt| stmt.is_function_def_stmt()) - else { - return; + }) => { + let Some(parent_arg) = parent_parameters.args.first() else { + return; + }; + + ( + parent_arg.name().as_str(), + has_local_dunder_class_var_ref(callable_scope, |finder| { + finder.visit_expr(body); + }), + ) + } + _ => return, }; - if is_builtins_super(checker.semantic(), call) - && !has_local_dunder_class_var_ref(checker.semantic(), func_stmt) - { + if is_builtins_super(checker.semantic(), call) && !has_local_dunder_class_var_ref { return; } - // Extract the name of the first argument to the enclosing function. - let Some(parent_arg) = parent_parameters.args.first() else { - return; - }; - let mut enclosing_classes = checker.semantic().current_scopes().filter_map(|scope| { let ScopeKind::Class(class_def) = &scope.kind else { return None; @@ -136,7 +167,7 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall return; }; - if second_arg_id != parent_arg.name().as_str() { + if second_arg_id != parent_arg_name { return; } @@ -145,7 +176,7 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall // For `super(Outer.Inner, self)`, verify each segment matches the enclosing class nesting. match first_arg { Expr::Name(ast::ExprName { id, .. }) => { - if checker.semantic().current_scope().has(id) { + if callable_scope.has(id) { return; } @@ -263,18 +294,22 @@ 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. +/// Returns `true` if the callable body contains load references to `__class__` or `super` without +/// a 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__") { +/// This indicates that the callable 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( + callable_scope: &Scope, + visit: impl FnOnce(&mut ClassCellReferenceFinder), +) -> bool { + if callable_scope.has("__class__") { return false; } let mut finder = ClassCellReferenceFinder::new(); - finder.visit_stmt(func_stmt); + visit(&mut finder); finder.found() } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap index 942695a391367..3cd095b0d7207 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap @@ -552,52 +552,69 @@ help: Remove `super()` parameters 398 | class LambdaMethod(BaseClass): UP008 [*] Use `super()` instead of `super(__class__, self)` - --> UP008.py:406:14 + --> UP008.py:399:27 | -404 | @classmethod -405 | def f(cls): -406 | super(ClassMethod, cls).f() # can use super() +398 | class LambdaMethod(BaseClass): +399 | f = lambda self: super(LambdaMethod, self).f() # can use super() + | ^^^^^^^^^^^^^^^^^^^^ + | +help: Remove `super()` parameters +396 | +397 | +398 | class LambdaMethod(BaseClass): + - f = lambda self: super(LambdaMethod, self).f() # can use super() +399 + f = lambda self: super().f() # can use super() +400 | +401 | +402 | class ClassMethod(BaseClass): + +UP008 [*] Use `super()` instead of `super(__class__, self)` + --> UP008.py:405:14 + | +403 | @classmethod +404 | def f(cls): +405 | super(ClassMethod, cls).f() # can use super() | ^^^^^^^^^^^^^^^^^^ | help: Remove `super()` parameters -403 | class ClassMethod(BaseClass): -404 | @classmethod -405 | def f(cls): +402 | class ClassMethod(BaseClass): +403 | @classmethod +404 | def f(cls): - super(ClassMethod, cls).f() # can use super() -406 + super().f() # can use super() +405 + super().f() # can use super() +406 | 407 | -408 | -409 | class AsyncMethod(BaseClass): +408 | class AsyncMethod(BaseClass): UP008 [*] Use `super()` instead of `super(__class__, self)` - --> UP008.py:411:14 + --> UP008.py:410:14 | -409 | class AsyncMethod(BaseClass): -410 | async def f(self): -411 | super(AsyncMethod, self).f() # can use super() +408 | class AsyncMethod(BaseClass): +409 | async def f(self): +410 | super(AsyncMethod, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^ | help: Remove `super()` parameters -408 | -409 | class AsyncMethod(BaseClass): -410 | async def f(self): +407 | +408 | class AsyncMethod(BaseClass): +409 | async def f(self): - super(AsyncMethod, self).f() # can use super() -411 + super().f() # can use super() +410 + super().f() # can use super() +411 | 412 | -413 | -414 | class OuterWithWhitespace: +413 | class OuterWithWhitespace: UP008 [*] Use `super()` instead of `super(__class__, self)` - --> UP008.py:417:19 + --> UP008.py:416:19 | -415 | class Inner(BaseClass): -416 | def f(self): -417 | super (OuterWithWhitespace.Inner, self).f() # can use super() +414 | class Inner(BaseClass): +415 | def f(self): +416 | super (OuterWithWhitespace.Inner, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: Remove `super()` parameters -414 | class OuterWithWhitespace: -415 | class Inner(BaseClass): -416 | def f(self): +413 | class OuterWithWhitespace: +414 | class Inner(BaseClass): +415 | def f(self): - super (OuterWithWhitespace.Inner, self).f() # can use super() -417 + super ().f() # can use super() +416 + super ().f() # can use super()