diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py new file mode 100644 index 0000000000..558060bc0f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py @@ -0,0 +1,96 @@ +try: + 1 / 0 +except ValueError as e: + pass + + +try: + 1 / 0 +except ValueError as e: + print(e) + + +def f(): + x = 1 + y = 2 + z = x + y + + +def f(): + foo = (1, 2) + (a, b) = (1, 2) + + bar = (1, 2) + (c, d) = bar + + (x, y) = baz = bar + + +def f(): + locals() + x = 1 + + +def f(): + _ = 1 + __ = 1 + _discarded = 1 + + +a = 1 + + +def f(): + global a + + # Used in `c` via `nonlocal`. + b = 1 + + def c(): + # F841 + b = 1 + + def d(): + nonlocal b + + +def f(): + annotations = [] + assert len([annotations for annotations in annotations]) + + +def f(): + def connect(): + return None, None + + with connect() as (connection, cursor): + cursor.execute("SELECT * FROM users") + + +def f(): + def connect(): + return None, None + + with connect() as (connection, cursor): + cursor.execute("SELECT * FROM users") + + +def f(): + with open("file") as my_file, open("") as ((this, that)): + print("hello") + + +def f(): + with ( + open("file") as my_file, + open("") as ((this, that)), + ): + print("hello") + + +def f(): + exponential, base_multiplier = 1, 2 + hash_map = { + (exponential := (exponential * base_multiplier) % 3): i + 1 for i in range(2) + } + return hash_map diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_1.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_1.py new file mode 100644 index 0000000000..85a60f1ce0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_1.py @@ -0,0 +1,24 @@ +def f(tup): + x, y = tup + + +def f(): + x, y = 1, 2 # this triggers RUF059 as it's just a simple assignment where unpacking isn't needed + + +def f(): + (x, y) = coords = 1, 2 + if x > 1: + print(coords) + + +def f(): + (x, y) = coords = 1, 2 + + +def f(): + coords = (x, y) = 1, 2 + + +def f(): + (a, b) = (x, y) = 1, 2 # this triggers RUF059 on everything diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_2.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_2.py new file mode 100644 index 0000000000..730412c3f0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_2.py @@ -0,0 +1,31 @@ +"""Test case for fixing RUF059 violations.""" + + +def f(): + with foo() as x1: + pass + + with foo() as (x2, y2): + pass + + with (foo() as x3, foo() as y3, foo() as z3): + pass + + +def f(): + (x1, y1) = (1, 2) + (x2, y2) = coords2 = (1, 2) + coords3 = (x3, y3) = (1, 2) + + +def f(): + with Nested(m) as (x, y): + pass + + +def f(): + toplevel = (a, b) = lexer.get_token() + + +def f(): + (a, b) = toplevel = lexer.get_token() diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F841_4.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_3.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyflakes/F841_4.py rename to crates/ruff_linter/resources/test/fixtures/ruff/RUF059_3.py diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index d5801e9dba..36a8af20fa 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -48,6 +48,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { Rule::UnusedPrivateTypedDict, Rule::UnusedPrivateTypeVar, Rule::UnusedStaticMethodArgument, + Rule::UnusedUnpackedVariable, Rule::UnusedVariable, ]) { return; @@ -390,8 +391,43 @@ pub(crate) fn deferred_scopes(checker: &Checker) { } if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) { - if checker.enabled(Rule::UnusedVariable) { - pyflakes::rules::unused_variable(checker, scope); + if checker.any_enabled(&[Rule::UnusedVariable, Rule::UnusedUnpackedVariable]) + && !(scope.uses_locals() && scope.kind.is_function()) + { + let unused_bindings = scope + .bindings() + .map(|(name, binding_id)| (name, checker.semantic().binding(binding_id))) + .filter_map(|(name, binding)| { + if (binding.kind.is_assignment() + || binding.kind.is_named_expr_assignment() + || binding.kind.is_with_item_var()) + && binding.is_unused() + && !binding.is_nonlocal() + && !binding.is_global() + && !checker.settings.dummy_variable_rgx.is_match(name) + && !matches!( + name, + "__tracebackhide__" + | "__traceback_info__" + | "__traceback_supplement__" + | "__debuggerskip__" + ) + { + return Some((name, binding)); + } + + None + }); + + for (unused_name, unused_binding) in unused_bindings { + if checker.enabled(Rule::UnusedVariable) { + pyflakes::rules::unused_variable(checker, unused_name, unused_binding); + } + + if checker.enabled(Rule::UnusedUnpackedVariable) { + ruff::rules::unused_unpacked_variable(checker, unused_name, unused_binding); + } + } } if checker.enabled(Rule::UnusedAnnotation) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 9a8971e691..c0e541fe57 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1012,6 +1012,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback), (Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound), (Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip), + (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap index f399be9a82..327ab346a3 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_linter/src/message/sarif.rs expression: value -snapshot_kind: text --- { "$schema": "https://json.schemastore.org/sarif-2.1.0.json", @@ -120,7 +119,7 @@ snapshot_kind: text }, { "fullDescription": { - "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\nUnder [preview mode](https://docs.astral.sh/ruff/preview), this rule also\ntriggers on unused unpacked assignments (for example, `x, y = foo()`).\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Options\n- `lint.dummy-variable-rgx`\n" + "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Options\n- `lint.dummy-variable-rgx`\n" }, "help": { "text": "Local variable `{name}` is assigned to but never used" diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 4f00f15b8d..6d1cedfd08 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -178,7 +178,6 @@ mod tests { #[test_case(Rule::UnusedVariable, Path::new("F841_1.py"))] #[test_case(Rule::UnusedVariable, Path::new("F841_2.py"))] #[test_case(Rule::UnusedVariable, Path::new("F841_3.py"))] - #[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))] #[test_case(Rule::UnusedAnnotation, Path::new("F842.py"))] #[test_case(Rule::RaiseNotImplemented, Path::new("F901.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -226,7 +225,6 @@ mod tests { assert_messages!(diagnostics); } - #[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))] #[test_case(Rule::UnusedImport, Path::new("__init__.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_25__all_nonempty/__init__.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index b175d7c360..dc7d7e208c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -6,7 +6,7 @@ use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Stmt}; use ruff_python_parser::{TokenKind, Tokens}; -use ruff_python_semantic::{Binding, Scope}; +use ruff_python_semantic::Binding; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -23,9 +23,6 @@ use crate::fix::edits::delete_stmt; /// prefixed with an underscore, or some other value that adheres to the /// [`lint.dummy-variable-rgx`] pattern. /// -/// Under [preview mode](https://docs.astral.sh/ruff/preview), this rule also -/// triggers on unused unpacked assignments (for example, `x, y = foo()`). -/// /// ## Example /// ```python /// def foo(): @@ -249,47 +246,19 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { } /// F841 -pub(crate) fn unused_variable(checker: &Checker, scope: &Scope) { - if scope.uses_locals() && scope.kind.is_function() { +pub(crate) fn unused_variable(checker: &Checker, name: &str, binding: &Binding) { + if binding.is_unpacked_assignment() { return; } - for (name, binding) in scope - .bindings() - .map(|(name, binding_id)| (name, checker.semantic().binding(binding_id))) - .filter_map(|(name, binding)| { - if (binding.kind.is_assignment() - || binding.kind.is_named_expr_assignment() - || binding.kind.is_with_item_var()) - // Stabilization depends on resolving https://github.com/astral-sh/ruff/issues/8884 - && (!binding.is_unpacked_assignment() || checker.settings.preview.is_enabled()) - && binding.is_unused() - && !binding.is_nonlocal() - && !binding.is_global() - && !checker.settings.dummy_variable_rgx.is_match(name) - && !matches!( - name, - "__tracebackhide__" - | "__traceback_info__" - | "__traceback_supplement__" - | "__debuggerskip__" - ) - { - return Some((name, binding)); - } - - None - }) - { - let mut diagnostic = Diagnostic::new( - UnusedVariable { - name: name.to_string(), - }, - binding.range(), - ); - if let Some(fix) = remove_unused_variable(binding, checker) { - diagnostic.set_fix(fix); - } - checker.report_diagnostic(diagnostic); + let mut diagnostic = Diagnostic::new( + UnusedVariable { + name: name.to_string(), + }, + binding.range(), + ); + if let Some(fix) = remove_unused_variable(binding, checker) { + diagnostic.set_fix(fix); } + checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_4.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_4.py.snap deleted file mode 100644 index c9ece5c6a9..0000000000 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F841_F841_4.py.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyflakes/mod.rs -snapshot_kind: text ---- -F841_4.py:12:5: F841 [*] Local variable `a` is assigned to but never used - | -11 | def bar(): -12 | a = foo() - | ^ F841 -13 | b, c = foo() - | - = help: Remove assignment to unused variable `a` - -ℹ Unsafe fix -9 9 | -10 10 | -11 11 | def bar(): -12 |- a = foo() - 12 |+ foo() -13 13 | b, c = foo() -14 14 | -15 15 | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F841_F841_4.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F841_F841_4.py.snap deleted file mode 100644 index 7a0074e512..0000000000 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F841_F841_4.py.snap +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyflakes/mod.rs -snapshot_kind: text ---- -F841_4.py:12:5: F841 [*] Local variable `a` is assigned to but never used - | -11 | def bar(): -12 | a = foo() - | ^ F841 -13 | b, c = foo() - | - = help: Remove assignment to unused variable `a` - -ℹ Unsafe fix -9 9 | -10 10 | -11 11 | def bar(): -12 |- a = foo() - 12 |+ foo() -13 13 | b, c = foo() -14 14 | -15 15 | - -F841_4.py:13:5: F841 [*] Local variable `b` is assigned to but never used - | -11 | def bar(): -12 | a = foo() -13 | b, c = foo() - | ^ F841 - | - = help: Remove assignment to unused variable `b` - -ℹ Unsafe fix -10 10 | -11 11 | def bar(): -12 12 | a = foo() -13 |- b, c = foo() - 13 |+ _b, c = foo() -14 14 | -15 15 | -16 16 | def baz(): - -F841_4.py:13:8: F841 [*] Local variable `c` is assigned to but never used - | -11 | def bar(): -12 | a = foo() -13 | b, c = foo() - | ^ F841 - | - = help: Remove assignment to unused variable `c` - -ℹ Unsafe fix -10 10 | -11 11 | def bar(): -12 12 | a = foo() -13 |- b, c = foo() - 13 |+ b, _c = foo() -14 14 | -15 15 | -16 16 | def baz(): diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 2513a8e0a7..6831ca274a 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -90,6 +90,10 @@ mod tests { #[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))] #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))] #[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))] + #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_0.py"))] + #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))] + #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_2.py"))] + #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_3.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 5a603b6bc3..c3b15f3565 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -50,6 +50,7 @@ pub(crate) use unraw_re_pattern::*; pub(crate) use unsafe_markup_use::*; pub(crate) use unused_async::*; pub(crate) use unused_noqa::*; +pub(crate) use unused_unpacked_variable::*; pub(crate) use used_dummy_variable::*; pub(crate) use useless_if_else::*; pub(crate) use zip_instead_of_pairwise::*; @@ -110,6 +111,7 @@ mod unraw_re_pattern; mod unsafe_markup_use; mod unused_async; mod unused_noqa; +mod unused_unpacked_variable; mod used_dummy_variable; mod useless_if_else; mod zip_instead_of_pairwise; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs new file mode 100644 index 0000000000..3229e33665 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs @@ -0,0 +1,91 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_semantic::Binding; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for the presence of unused variables in unpacked assignments. +/// +/// ## Why is this bad? +/// A variable that is defined but never used can confuse readers. +/// +/// If a variable is intentionally defined-but-not-used, it should be +/// prefixed with an underscore, or some other value that adheres to the +/// [`lint.dummy-variable-rgx`] pattern. +/// +/// ## Example +/// +/// ```python +/// def get_pair(): +/// return 1, 2 +/// +/// +/// def foo(): +/// x, y = get_pair() +/// return x +/// ``` +/// +/// Use instead: +/// +/// ```python +/// def foo(): +/// x, _ = get_pair() +/// return x +/// ``` +/// +/// ## Options +/// - `lint.dummy-variable-rgx` +#[derive(ViolationMetadata)] +pub(crate) struct UnusedUnpackedVariable { + pub name: String, +} + +impl Violation for UnusedUnpackedVariable { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UnusedUnpackedVariable { name } = self; + format!("Unpacked variable `{name}` is never used") + } + + fn fix_title(&self) -> Option { + Some("Prefix it with an underscore or any other dummy variable pattern".to_string()) + } +} + +/// Generate a [`Edit`] to remove an unused variable assignment to a [`Binding`]. +fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { + let node_id = binding.source?; + let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id)); + + let name = binding.name(checker.source()); + let renamed = format!("_{name}"); + if checker.settings.dummy_variable_rgx.is_match(&renamed) { + let edit = Edit::range_replacement(renamed, binding.range()); + + return Some(Fix::unsafe_edit(edit).isolate(isolation)); + } + + None +} + +/// RUF059 +pub(crate) fn unused_unpacked_variable(checker: &Checker, name: &str, binding: &Binding) { + if !binding.is_unpacked_assignment() { + return; + } + + let mut diagnostic = Diagnostic::new( + UnusedUnpackedVariable { + name: name.to_string(), + }, + binding.range(), + ); + if let Some(fix) = remove_unused_variable(binding, checker) { + diagnostic.set_fix(fix); + } + checker.report_diagnostic(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap new file mode 100644 index 0000000000..03d9674ce0 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap @@ -0,0 +1,200 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF059_0.py:24:6: RUF059 [*] Unpacked variable `c` is never used + | +23 | bar = (1, 2) +24 | (c, d) = bar + | ^ RUF059 +25 | +26 | (x, y) = baz = bar + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +21 21 | (a, b) = (1, 2) +22 22 | +23 23 | bar = (1, 2) +24 |- (c, d) = bar + 24 |+ (_c, d) = bar +25 25 | +26 26 | (x, y) = baz = bar +27 27 | + +RUF059_0.py:24:9: RUF059 [*] Unpacked variable `d` is never used + | +23 | bar = (1, 2) +24 | (c, d) = bar + | ^ RUF059 +25 | +26 | (x, y) = baz = bar + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +21 21 | (a, b) = (1, 2) +22 22 | +23 23 | bar = (1, 2) +24 |- (c, d) = bar + 24 |+ (c, _d) = bar +25 25 | +26 26 | (x, y) = baz = bar +27 27 | + +RUF059_0.py:26:6: RUF059 [*] Unpacked variable `x` is never used + | +24 | (c, d) = bar +25 | +26 | (x, y) = baz = bar + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +23 23 | bar = (1, 2) +24 24 | (c, d) = bar +25 25 | +26 |- (x, y) = baz = bar + 26 |+ (_x, y) = baz = bar +27 27 | +28 28 | +29 29 | def f(): + +RUF059_0.py:26:9: RUF059 [*] Unpacked variable `y` is never used + | +24 | (c, d) = bar +25 | +26 | (x, y) = baz = bar + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +23 23 | bar = (1, 2) +24 24 | (c, d) = bar +25 25 | +26 |- (x, y) = baz = bar + 26 |+ (x, _y) = baz = bar +27 27 | +28 28 | +29 29 | def f(): + +RUF059_0.py:66:24: RUF059 [*] Unpacked variable `connection` is never used + | +64 | return None, None +65 | +66 | with connect() as (connection, cursor): + | ^^^^^^^^^^ RUF059 +67 | cursor.execute("SELECT * FROM users") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +63 63 | def connect(): +64 64 | return None, None +65 65 | +66 |- with connect() as (connection, cursor): + 66 |+ with connect() as (_connection, cursor): +67 67 | cursor.execute("SELECT * FROM users") +68 68 | +69 69 | + +RUF059_0.py:74:24: RUF059 [*] Unpacked variable `connection` is never used + | +72 | return None, None +73 | +74 | with connect() as (connection, cursor): + | ^^^^^^^^^^ RUF059 +75 | cursor.execute("SELECT * FROM users") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +71 71 | def connect(): +72 72 | return None, None +73 73 | +74 |- with connect() as (connection, cursor): + 74 |+ with connect() as (_connection, cursor): +75 75 | cursor.execute("SELECT * FROM users") +76 76 | +77 77 | + +RUF059_0.py:79:49: RUF059 [*] Unpacked variable `this` is never used + | +78 | def f(): +79 | with open("file") as my_file, open("") as ((this, that)): + | ^^^^ RUF059 +80 | print("hello") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +76 76 | +77 77 | +78 78 | def f(): +79 |- with open("file") as my_file, open("") as ((this, that)): + 79 |+ with open("file") as my_file, open("") as ((_this, that)): +80 80 | print("hello") +81 81 | +82 82 | + +RUF059_0.py:79:55: RUF059 [*] Unpacked variable `that` is never used + | +78 | def f(): +79 | with open("file") as my_file, open("") as ((this, that)): + | ^^^^ RUF059 +80 | print("hello") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +76 76 | +77 77 | +78 78 | def f(): +79 |- with open("file") as my_file, open("") as ((this, that)): + 79 |+ with open("file") as my_file, open("") as ((this, _that)): +80 80 | print("hello") +81 81 | +82 82 | + +RUF059_0.py:86:23: RUF059 [*] Unpacked variable `this` is never used + | +84 | with ( +85 | open("file") as my_file, +86 | open("") as ((this, that)), + | ^^^^ RUF059 +87 | ): +88 | print("hello") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +83 83 | def f(): +84 84 | with ( +85 85 | open("file") as my_file, +86 |- open("") as ((this, that)), + 86 |+ open("") as ((_this, that)), +87 87 | ): +88 88 | print("hello") +89 89 | + +RUF059_0.py:86:29: RUF059 [*] Unpacked variable `that` is never used + | +84 | with ( +85 | open("file") as my_file, +86 | open("") as ((this, that)), + | ^^^^ RUF059 +87 | ): +88 | print("hello") + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +83 83 | def f(): +84 84 | with ( +85 85 | open("file") as my_file, +86 |- open("") as ((this, that)), + 86 |+ open("") as ((this, _that)), +87 87 | ): +88 88 | print("hello") +89 89 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap new file mode 100644 index 0000000000..4172040bf2 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap @@ -0,0 +1,126 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF059_1.py:2:5: RUF059 [*] Unpacked variable `x` is never used + | +1 | def f(tup): +2 | x, y = tup + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +1 1 | def f(tup): +2 |- x, y = tup + 2 |+ _x, y = tup +3 3 | +4 4 | +5 5 | def f(): + +RUF059_1.py:2:8: RUF059 [*] Unpacked variable `y` is never used + | +1 | def f(tup): +2 | x, y = tup + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +1 1 | def f(tup): +2 |- x, y = tup + 2 |+ x, _y = tup +3 3 | +4 4 | +5 5 | def f(): + +RUF059_1.py:10:9: RUF059 [*] Unpacked variable `y` is never used + | + 9 | def f(): +10 | (x, y) = coords = 1, 2 + | ^ RUF059 +11 | if x > 1: +12 | print(coords) + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +7 7 | +8 8 | +9 9 | def f(): +10 |- (x, y) = coords = 1, 2 + 10 |+ (x, _y) = coords = 1, 2 +11 11 | if x > 1: +12 12 | print(coords) +13 13 | + +RUF059_1.py:16:6: RUF059 [*] Unpacked variable `x` is never used + | +15 | def f(): +16 | (x, y) = coords = 1, 2 + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def f(): +16 |- (x, y) = coords = 1, 2 + 16 |+ (_x, y) = coords = 1, 2 +17 17 | +18 18 | +19 19 | def f(): + +RUF059_1.py:16:9: RUF059 [*] Unpacked variable `y` is never used + | +15 | def f(): +16 | (x, y) = coords = 1, 2 + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def f(): +16 |- (x, y) = coords = 1, 2 + 16 |+ (x, _y) = coords = 1, 2 +17 17 | +18 18 | +19 19 | def f(): + +RUF059_1.py:20:15: RUF059 [*] Unpacked variable `x` is never used + | +19 | def f(): +20 | coords = (x, y) = 1, 2 + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def f(): +20 |- coords = (x, y) = 1, 2 + 20 |+ coords = (_x, y) = 1, 2 +21 21 | +22 22 | +23 23 | def f(): + +RUF059_1.py:20:18: RUF059 [*] Unpacked variable `y` is never used + | +19 | def f(): +20 | coords = (x, y) = 1, 2 + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def f(): +20 |- coords = (x, y) = 1, 2 + 20 |+ coords = (x, _y) = 1, 2 +21 21 | +22 22 | +23 23 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap new file mode 100644 index 0000000000..b7f265321e --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap @@ -0,0 +1,224 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF059_2.py:8:20: RUF059 [*] Unpacked variable `x2` is never used + | +6 | pass +7 | +8 | with foo() as (x2, y2): + | ^^ RUF059 +9 | pass + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +5 5 | with foo() as x1: +6 6 | pass +7 7 | +8 |- with foo() as (x2, y2): + 8 |+ with foo() as (_x2, y2): +9 9 | pass +10 10 | +11 11 | with (foo() as x3, foo() as y3, foo() as z3): + +RUF059_2.py:8:24: RUF059 [*] Unpacked variable `y2` is never used + | +6 | pass +7 | +8 | with foo() as (x2, y2): + | ^^ RUF059 +9 | pass + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +5 5 | with foo() as x1: +6 6 | pass +7 7 | +8 |- with foo() as (x2, y2): + 8 |+ with foo() as (x2, _y2): +9 9 | pass +10 10 | +11 11 | with (foo() as x3, foo() as y3, foo() as z3): + +RUF059_2.py:17:6: RUF059 [*] Unpacked variable `x2` is never used + | +15 | def f(): +16 | (x1, y1) = (1, 2) +17 | (x2, y2) = coords2 = (1, 2) + | ^^ RUF059 +18 | coords3 = (x3, y3) = (1, 2) + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +14 14 | +15 15 | def f(): +16 16 | (x1, y1) = (1, 2) +17 |- (x2, y2) = coords2 = (1, 2) + 17 |+ (_x2, y2) = coords2 = (1, 2) +18 18 | coords3 = (x3, y3) = (1, 2) +19 19 | +20 20 | + +RUF059_2.py:17:10: RUF059 [*] Unpacked variable `y2` is never used + | +15 | def f(): +16 | (x1, y1) = (1, 2) +17 | (x2, y2) = coords2 = (1, 2) + | ^^ RUF059 +18 | coords3 = (x3, y3) = (1, 2) + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +14 14 | +15 15 | def f(): +16 16 | (x1, y1) = (1, 2) +17 |- (x2, y2) = coords2 = (1, 2) + 17 |+ (x2, _y2) = coords2 = (1, 2) +18 18 | coords3 = (x3, y3) = (1, 2) +19 19 | +20 20 | + +RUF059_2.py:18:16: RUF059 [*] Unpacked variable `x3` is never used + | +16 | (x1, y1) = (1, 2) +17 | (x2, y2) = coords2 = (1, 2) +18 | coords3 = (x3, y3) = (1, 2) + | ^^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +15 15 | def f(): +16 16 | (x1, y1) = (1, 2) +17 17 | (x2, y2) = coords2 = (1, 2) +18 |- coords3 = (x3, y3) = (1, 2) + 18 |+ coords3 = (_x3, y3) = (1, 2) +19 19 | +20 20 | +21 21 | def f(): + +RUF059_2.py:18:20: RUF059 [*] Unpacked variable `y3` is never used + | +16 | (x1, y1) = (1, 2) +17 | (x2, y2) = coords2 = (1, 2) +18 | coords3 = (x3, y3) = (1, 2) + | ^^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +15 15 | def f(): +16 16 | (x1, y1) = (1, 2) +17 17 | (x2, y2) = coords2 = (1, 2) +18 |- coords3 = (x3, y3) = (1, 2) + 18 |+ coords3 = (x3, _y3) = (1, 2) +19 19 | +20 20 | +21 21 | def f(): + +RUF059_2.py:22:24: RUF059 [*] Unpacked variable `x` is never used + | +21 | def f(): +22 | with Nested(m) as (x, y): + | ^ RUF059 +23 | pass + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +19 19 | +20 20 | +21 21 | def f(): +22 |- with Nested(m) as (x, y): + 22 |+ with Nested(m) as (_x, y): +23 23 | pass +24 24 | +25 25 | + +RUF059_2.py:22:27: RUF059 [*] Unpacked variable `y` is never used + | +21 | def f(): +22 | with Nested(m) as (x, y): + | ^ RUF059 +23 | pass + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +19 19 | +20 20 | +21 21 | def f(): +22 |- with Nested(m) as (x, y): + 22 |+ with Nested(m) as (x, _y): +23 23 | pass +24 24 | +25 25 | + +RUF059_2.py:27:17: RUF059 [*] Unpacked variable `a` is never used + | +26 | def f(): +27 | toplevel = (a, b) = lexer.get_token() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +24 24 | +25 25 | +26 26 | def f(): +27 |- toplevel = (a, b) = lexer.get_token() + 27 |+ toplevel = (_a, b) = lexer.get_token() +28 28 | +29 29 | +30 30 | def f(): + +RUF059_2.py:27:20: RUF059 [*] Unpacked variable `b` is never used + | +26 | def f(): +27 | toplevel = (a, b) = lexer.get_token() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +24 24 | +25 25 | +26 26 | def f(): +27 |- toplevel = (a, b) = lexer.get_token() + 27 |+ toplevel = (a, _b) = lexer.get_token() +28 28 | +29 29 | +30 30 | def f(): + +RUF059_2.py:31:6: RUF059 [*] Unpacked variable `a` is never used + | +30 | def f(): +31 | (a, b) = toplevel = lexer.get_token() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +28 28 | +29 29 | +30 30 | def f(): +31 |- (a, b) = toplevel = lexer.get_token() + 31 |+ (_a, b) = toplevel = lexer.get_token() + +RUF059_2.py:31:9: RUF059 [*] Unpacked variable `b` is never used + | +30 | def f(): +31 | (a, b) = toplevel = lexer.get_token() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +28 28 | +29 29 | +30 30 | def f(): +31 |- (a, b) = toplevel = lexer.get_token() + 31 |+ (a, _b) = toplevel = lexer.get_token() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap new file mode 100644 index 0000000000..754acdcc6f --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF059_3.py:13:5: RUF059 [*] Unpacked variable `b` is never used + | +11 | def bar(): +12 | a = foo() +13 | b, c = foo() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +10 10 | +11 11 | def bar(): +12 12 | a = foo() +13 |- b, c = foo() + 13 |+ _b, c = foo() +14 14 | +15 15 | +16 16 | def baz(): + +RUF059_3.py:13:8: RUF059 [*] Unpacked variable `c` is never used + | +11 | def bar(): +12 | a = foo() +13 | b, c = foo() + | ^ RUF059 + | + = help: Prefix it with an underscore or any other dummy variable pattern + +ℹ Unsafe fix +10 10 | +11 11 | def bar(): +12 12 | a = foo() +13 |- b, c = foo() + 13 |+ b, _c = foo() +14 14 | +15 15 | +16 16 | def baz(): diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 1a0a2bb597..e3829f177b 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -445,7 +445,8 @@ fn tags(code: &str) -> Option> { match code { // F401: imported but unused // F841: local variable is assigned to but never used - "F401" | "F841" => Some(vec![lsp_types::DiagnosticTag::UNNECESSARY]), + // RUF059: Unused unpacked variable + "F401" | "F841" | "RUF059" => Some(vec![lsp_types::DiagnosticTag::UNNECESSARY]), _ => None, } } diff --git a/ruff.schema.json b/ruff.schema.json index 57dd60a7c4..05d88ec10a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3972,6 +3972,7 @@ "RUF056", "RUF057", "RUF058", + "RUF059", "RUF1", "RUF10", "RUF100",