diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py index 9a8fd4c83585c..fbebdff4b2b61 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py @@ -55,6 +55,18 @@ def f(cls, x): def f(x): print("Hello, world!") + def f(self, x): + msg[0] = "..." + raise NotImplementedError(msg) + + def f(self, x): + msg = "..." + raise NotImplementedError(foo) + + def f(self, x): + msg = "..." + raise NotImplementedError("must use msg") + ### # Unused arguments attached to empty functions (OK). ### @@ -107,6 +119,15 @@ def f(self, x): def f(self, x): raise NotImplemented("...") + def f(self, x): + msg = "..." + raise NotImplementedError(msg) + + def f(self, x, y): + """Docstring.""" + msg = f"{x}..." + raise NotImplementedError(msg) + ### # Unused functions attached to abstract methods (OK). ### diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index beabe2e33917f..6361f2d785644 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -1,6 +1,6 @@ use regex::Regex; use ruff_python_ast as ast; -use ruff_python_ast::{Parameter, Parameters}; +use ruff_python_ast::{Parameter, Parameters, Stmt, StmtExpr, StmtFunctionDef, StmtRaise}; use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::{Diagnostic, Violation}; @@ -311,6 +311,63 @@ fn call<'a>( })); } +/// Returns `true` if a function appears to be a base class stub. In other +/// words, if it matches the following syntax: +/// +/// ```text +/// variable = +/// raise NotImplementedError(variable) +/// ``` +/// +/// See also [`is_stub`]. We're a bit more generous in what is considered a +/// stub in this rule to avoid clashing with [`EM101`]. +/// +/// [`is_stub`]: function_type::is_stub +/// [`EM101`]: https://docs.astral.sh/ruff/rules/raw-string-in-exception/ +fn is_not_implemented_stub_with_variable( + function_def: &StmtFunctionDef, + semantic: &SemanticModel, +) -> bool { + // Ignore doc-strings. + let statements = match function_def.body.as_slice() { + [Stmt::Expr(StmtExpr { value, .. }), rest @ ..] if value.is_string_literal_expr() => rest, + _ => &function_def.body, + }; + + let [Stmt::Assign(ast::StmtAssign { targets, value, .. }), Stmt::Raise(StmtRaise { + exc: Some(exception), + .. + })] = statements + else { + return false; + }; + + if !matches!(**value, ast::Expr::StringLiteral(_) | ast::Expr::FString(_)) { + return false; + } + + let ast::Expr::Call(ast::ExprCall { + func, arguments, .. + }) = &**exception + else { + return false; + }; + + if !semantic.match_builtin_expr(func, "NotImplementedError") { + return false; + } + + let [argument] = &*arguments.args else { + return false; + }; + + let [target] = targets.as_slice() else { + return false; + }; + + argument.as_name_expr().map(ast::ExprName::id) == target.as_name_expr().map(ast::ExprName::id) +} + /// ARG001, ARG002, ARG003, ARG004, ARG005 pub(crate) fn unused_arguments( checker: &Checker, @@ -345,6 +402,7 @@ pub(crate) fn unused_arguments( function_type::FunctionType::Function => { if checker.enabled(Argumentable::Function.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) + && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && !visibility::is_overload(decorator_list, checker.semantic()) { function( @@ -364,6 +422,7 @@ pub(crate) fn unused_arguments( function_type::FunctionType::Method => { if checker.enabled(Argumentable::Method.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) + && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) @@ -389,6 +448,7 @@ pub(crate) fn unused_arguments( function_type::FunctionType::ClassMethod => { if checker.enabled(Argumentable::ClassMethod.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) + && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) @@ -414,6 +474,7 @@ pub(crate) fn unused_arguments( function_type::FunctionType::StaticMethod => { if checker.enabled(Argumentable::StaticMethod.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) + && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap index e235a37a5394c..d04530343580f 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap @@ -28,13 +28,41 @@ ARG.py:43:16: ARG002 Unused method argument: `x` 44 | print("Hello, world!") | -ARG.py:192:24: ARG002 Unused method argument: `x` +ARG.py:58:17: ARG002 Unused method argument: `x` + | +56 | print("Hello, world!") +57 | +58 | def f(self, x): + | ^ ARG002 +59 | msg[0] = "..." +60 | raise NotImplementedError(msg) + | + +ARG.py:62:17: ARG002 Unused method argument: `x` + | +60 | raise NotImplementedError(msg) +61 | +62 | def f(self, x): + | ^ ARG002 +63 | msg = "..." +64 | raise NotImplementedError(foo) + | + +ARG.py:66:17: ARG002 Unused method argument: `x` + | +64 | raise NotImplementedError(foo) +65 | +66 | def f(self, x): + | ^ ARG002 +67 | msg = "..." +68 | raise NotImplementedError("must use msg") + | + +ARG.py:213:24: ARG002 Unused method argument: `x` | -190 | ### -191 | class C: -192 | def __init__(self, x) -> None: +211 | ### +212 | class C: +213 | def __init__(self, x) -> None: | ^ ARG002 -193 | print("Hello, world!") +214 | print("Hello, world!") | - -