From 712ed7b12f051fd0435f232a43f23af1ce0bd3dd Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 6 Jun 2026 12:33:23 -0400 Subject: [PATCH 1/5] Only apply C409 tuple comprehension preview behaviour on Python 3.14+ --- .../flake8_comprehensions/C409_py313.py | 1 + .../src/rules/flake8_comprehensions/mod.rs | 22 +++++++++++++++++++ .../unnecessary_literal_within_tuple_call.rs | 17 ++++++++------ ...s__tests__preview__C409_C409_py313.py.snap | 10 +++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py create mode 100644 crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py new file mode 100644 index 00000000000000..18d2fe2f1da4fb --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py @@ -0,0 +1 @@ +tuple([x for x in range(10)]) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 23447713ddf668..fbb46f33bbdf23 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -87,6 +87,28 @@ mod tests { Ok(()) } + #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409_py313.py"))] + fn preview_rules_py313(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + assert_diagnostics_diff!( + snapshot, + Path::new("flake8_comprehensions").join(path).as_path(), + &LinterSettings { + preview: PreviewMode::Disabled, + ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313) + }, + &LinterSettings { + preview: PreviewMode::Enabled, + ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313) + }, + ); + Ok(()) + } + #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409_py315.py"))] fn preview_rules_py315(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index b7ed6f0a983135..d17833c6fede5a 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -1,6 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{self as ast, Expr, PythonVersion}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -13,8 +13,8 @@ use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `tuple` calls that take unnecessary list or tuple literals as -/// arguments. In [preview], this also includes unnecessary list comprehensions -/// within tuple calls. +/// arguments. In [preview], when targeting Python 3.14+, this also includes +/// unnecessary list comprehensions within tuple calls. /// /// ## Why is this bad? /// It's unnecessary to use a list or tuple literal within a `tuple()` call, @@ -24,9 +24,9 @@ use crate::rules::flake8_comprehensions::helpers; /// literal. Otherwise, if a tuple literal was passed, then the outer call /// to `tuple()` should be removed. /// -/// In [preview], this rule also checks for list comprehensions within `tuple()` -/// calls. If a list comprehension is found, it should be rewritten as a -/// generator expression. +/// In [preview], when targeting Python 3.14+, this rule also checks for list +/// comprehensions within `tuple()` calls. If a list comprehension is found, +/// it should be rewritten as a generator expression. /// /// ## Example /// ```python @@ -104,7 +104,10 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let argument_kind = match argument { Expr::Tuple(_) => TupleLiteralKind::Tuple, Expr::List(_) => TupleLiteralKind::List, - Expr::ListComp(_) if is_check_comprehensions_in_tuple_call_enabled(checker.settings()) => { + Expr::ListComp(_) + if is_check_comprehensions_in_tuple_call_enabled(checker.settings()) + && checker.target_version() >= PythonVersion::PY314 => + { TupleLiteralKind::ListComp } _ => return, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap new file mode 100644 index 00000000000000..418db92027b68b --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 0 +Added: 0 From f5876e253dcb5982fb1015631b83e801efa454a5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 7 Jul 2026 12:26:58 -0400 Subject: [PATCH 2/5] Remove C409 preview behavior with tuple comprehension --- .../flake8_comprehensions/C409_py313.py | 1 - .../flake8_comprehensions/C409_py315.py | 3 - .../src/checkers/ast/analyze/expression.rs | 2 +- crates/ruff_linter/src/preview.rs | 7 - .../src/rules/flake8_comprehensions/mod.rs | 45 --- .../unnecessary_literal_within_tuple_call.rs | 46 +-- ...ensions__tests__preview__C409_C409.py.snap | 379 ------------------ ...s__tests__preview__C409_C409_py313.py.snap | 10 - ...s__tests__preview__C409_C409_py315.py.snap | 21 - 9 files changed, 3 insertions(+), 511 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py delete mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py delete mode 100644 crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py315.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py deleted file mode 100644 index 18d2fe2f1da4fb..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py313.py +++ /dev/null @@ -1 +0,0 @@ -tuple([x for x in range(10)]) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py deleted file mode 100644 index c31654483296d7..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py +++ /dev/null @@ -1,3 +0,0 @@ -xs = [[1], [2]] - -tuple([*x for x in xs]) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 94dc18976bf47d..851d79547bcda2 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -923,7 +923,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinTupleCall) { flake8_comprehensions::rules::unnecessary_literal_within_tuple_call( - checker, expr, call, + checker, call, ); } if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinListCall) { diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 6cc1da7593707e..95dd24719a0e9a 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -41,13 +41,6 @@ pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterS settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/12657 -pub(crate) const fn is_check_comprehensions_in_tuple_call_enabled( - settings: &LinterSettings, -) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/issues/15347 pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index fbb46f33bbdf23..4e128403c514db 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -68,7 +68,6 @@ mod tests { Ok(()) } - #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409.py"))] #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_1.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( @@ -87,50 +86,6 @@ mod tests { Ok(()) } - #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409_py313.py"))] - fn preview_rules_py313(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview__{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - assert_diagnostics_diff!( - snapshot, - Path::new("flake8_comprehensions").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313) - }, - ); - Ok(()) - } - - #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409_py315.py"))] - fn preview_rules_py315(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview__{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - assert_diagnostics_diff!( - snapshot, - Path::new("flake8_comprehensions").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY315) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY315) - }, - ); - Ok(()) - } - #[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))] fn allow_dict_calls_with_keyword_arguments(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index d17833c6fede5a..e1872d1c1a562e 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -1,11 +1,9 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::helpers::any_over_expr; -use ruff_python_ast::{self as ast, Expr, PythonVersion}; +use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; -use crate::preview::is_check_comprehensions_in_tuple_call_enabled; use crate::rules::flake8_comprehensions::fixes; use crate::{Edit, Fix, FixAvailability, Violation}; @@ -13,8 +11,7 @@ use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `tuple` calls that take unnecessary list or tuple literals as -/// arguments. In [preview], when targeting Python 3.14+, this also includes -/// unnecessary list comprehensions within tuple calls. +/// arguments. /// /// ## Why is this bad? /// It's unnecessary to use a list or tuple literal within a `tuple()` call, @@ -24,29 +21,21 @@ use crate::rules::flake8_comprehensions::helpers; /// literal. Otherwise, if a tuple literal was passed, then the outer call /// to `tuple()` should be removed. /// -/// In [preview], when targeting Python 3.14+, this rule also checks for list -/// comprehensions within `tuple()` calls. If a list comprehension is found, -/// it should be rewritten as a generator expression. -/// /// ## Example /// ```python /// tuple([1, 2]) /// tuple((1, 2)) -/// tuple([x for x in range(10)]) /// ``` /// /// Use instead: /// ```python /// (1, 2) /// (1, 2) -/// tuple(x for x in range(10)) /// ``` /// /// ## Fix safety /// This rule's fix is marked as unsafe, as it may occasionally drop comments /// when rewriting the call. In most cases, though, comments will be preserved. -/// -/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.66")] pub(crate) struct UnnecessaryLiteralWithinTupleCall { @@ -67,10 +56,6 @@ impl Violation for UnnecessaryLiteralWithinTupleCall { "Unnecessary tuple literal passed to `tuple()` (remove the outer call to `tuple()`)" .to_string() } - TupleLiteralKind::ListComp => { - "Unnecessary list comprehension passed to `tuple()` (rewrite as a generator)" - .to_string() - } } } @@ -78,7 +63,6 @@ impl Violation for UnnecessaryLiteralWithinTupleCall { let title = match self.literal_kind { TupleLiteralKind::List => "Rewrite as a tuple literal", TupleLiteralKind::Tuple => "Remove the outer call to `tuple()`", - TupleLiteralKind::ListComp => "Rewrite as a generator", }; Some(title.to_string()) } @@ -87,7 +71,6 @@ impl Violation for UnnecessaryLiteralWithinTupleCall { /// C409 pub(crate) fn unnecessary_literal_within_tuple_call( checker: &Checker, - expr: &Expr, call: &ast::ExprCall, ) { if !call.arguments.keywords.is_empty() { @@ -104,12 +87,6 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let argument_kind = match argument { Expr::Tuple(_) => TupleLiteralKind::Tuple, Expr::List(_) => TupleLiteralKind::List, - Expr::ListComp(_) - if is_check_comprehensions_in_tuple_call_enabled(checker.settings()) - && checker.target_version() >= PythonVersion::PY314 => - { - TupleLiteralKind::ListComp - } _ => return, }; if !checker.semantic().has_builtin_binding("tuple") { @@ -157,24 +134,6 @@ pub(crate) fn unnecessary_literal_within_tuple_call( }); } - Expr::ListComp(ast::ExprListComp { elt, .. }) => { - if any_over_expr(elt, &Expr::is_await_expr) { - return; - } - if elt.is_starred_expr() { - // The LibCST-based fixer does not yet support PEP 798 unpacking comprehensions. - return; - } - // Convert `tuple([x for x in range(10)])` to `tuple(x for x in range(10))` - diagnostic.try_set_fix(|| { - fixes::fix_unnecessary_comprehension_in_call( - expr, - checker.locator(), - checker.stylist(), - ) - }); - } - _ => (), } } @@ -183,5 +142,4 @@ pub(crate) fn unnecessary_literal_within_tuple_call( enum TupleLiteralKind { List, Tuple, - ListComp, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409.py.snap deleted file mode 100644 index 3ee6174444f6c5..00000000000000 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409.py.snap +++ /dev/null @@ -1,379 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs ---- -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:1:6 - | -1 | t1 = tuple([]) - | ^^^^^^^^^ -2 | t2 = tuple([1, 2]) -3 | t3 = tuple((1, 2)) - | -help: Rewrite as a tuple literal - - t1 = tuple([]) -1 + t1 = () -2 | t2 = tuple([1, 2]) -3 | t3 = tuple((1, 2)) -4 | t4 = tuple([ -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:2:6 - | -1 | t1 = tuple([]) -2 | t2 = tuple([1, 2]) - | ^^^^^^^^^^^^^ -3 | t3 = tuple((1, 2)) -4 | t4 = tuple([ - | -help: Rewrite as a tuple literal -1 | t1 = tuple([]) - - t2 = tuple([1, 2]) -2 + t2 = (1, 2) -3 | t3 = tuple((1, 2)) -4 | t4 = tuple([ -5 | 1, -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary tuple literal passed to `tuple()` (remove the outer call to `tuple()`) - --> C409.py:3:6 - | -1 | t1 = tuple([]) -2 | t2 = tuple([1, 2]) -3 | t3 = tuple((1, 2)) - | ^^^^^^^^^^^^^ -4 | t4 = tuple([ -5 | 1, - | -help: Remove the outer call to `tuple()` -1 | t1 = tuple([]) -2 | t2 = tuple([1, 2]) - - t3 = tuple((1, 2)) -3 + t3 = (1, 2) -4 | t4 = tuple([ -5 | 1, -6 | 2 -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:4:6 - | -2 | t2 = tuple([1, 2]) -3 | t3 = tuple((1, 2)) -4 | t4 = tuple([ - | ______^ -5 | | 1, -6 | | 2 -7 | | ]) - | |__^ -8 | t5 = tuple( -9 | (1, 2) - | -help: Rewrite as a tuple literal -1 | t1 = tuple([]) -2 | t2 = tuple([1, 2]) -3 | t3 = tuple((1, 2)) - - t4 = tuple([ -4 + t4 = ( -5 | 1, -6 | 2 - - ]) -7 + ) -8 | t5 = tuple( -9 | (1, 2) -10 | ) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary tuple literal passed to `tuple()` (remove the outer call to `tuple()`) - --> C409.py:8:6 - | - 6 | 2 - 7 | ]) - 8 | t5 = tuple( - | ______^ - 9 | | (1, 2) -10 | | ) - | |_^ -11 | -12 | tuple( # comment - | -help: Remove the outer call to `tuple()` -5 | 1, -6 | 2 -7 | ]) - - t5 = tuple( - - (1, 2) - - ) -8 + t5 = (1, 2) -9 | -10 | tuple( # comment -11 | [1, 2] -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:12:1 - | -10 | ) -11 | -12 | / tuple( # comment -13 | | [1, 2] -14 | | ) - | |_^ -15 | -16 | tuple([ # comment - | -help: Rewrite as a tuple literal -9 | (1, 2) -10 | ) -11 | - - tuple( # comment - - [1, 2] - - ) -12 + (1, 2) -13 | -14 | tuple([ # comment -15 | 1, 2 -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:16:1 - | -14 | ) -15 | -16 | / tuple([ # comment -17 | | 1, 2 -18 | | ]) - | |__^ -19 | -20 | tuple(( - | -help: Rewrite as a tuple literal -13 | [1, 2] -14 | ) -15 | - - tuple([ # comment -16 + ( # comment -17 | 1, 2 - - ]) -18 + ) -19 | -20 | tuple(( -21 | 1, -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary tuple literal passed to `tuple()` (remove the outer call to `tuple()`) - --> C409.py:20:1 - | -18 | ]) -19 | -20 | / tuple(( -21 | | 1, -22 | | )) - | |__^ -23 | -24 | t6 = tuple([1]) - | -help: Remove the outer call to `tuple()` -17 | 1, 2 -18 | ]) -19 | - - tuple(( -20 + ( -21 | 1, - - )) -22 + ) -23 | -24 | t6 = tuple([1]) -25 | t7 = tuple((1,)) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:24:6 - | -22 | )) -23 | -24 | t6 = tuple([1]) - | ^^^^^^^^^^ -25 | t7 = tuple((1,)) -26 | t8 = tuple([1,]) - | -help: Rewrite as a tuple literal -21 | 1, -22 | )) -23 | - - t6 = tuple([1]) -24 + t6 = (1,) -25 | t7 = tuple((1,)) -26 | t8 = tuple([1,]) -27 | -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary tuple literal passed to `tuple()` (remove the outer call to `tuple()`) - --> C409.py:25:6 - | -24 | t6 = tuple([1]) -25 | t7 = tuple((1,)) - | ^^^^^^^^^^^ -26 | t8 = tuple([1,]) - | -help: Remove the outer call to `tuple()` -22 | )) -23 | -24 | t6 = tuple([1]) - - t7 = tuple((1,)) -25 + t7 = (1,) -26 | t8 = tuple([1,]) -27 | -28 | tuple([x for x in range(5)]) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:26:6 - | -24 | t6 = tuple([1]) -25 | t7 = tuple((1,)) -26 | t8 = tuple([1,]) - | ^^^^^^^^^^^ -27 | -28 | tuple([x for x in range(5)]) - | -help: Rewrite as a tuple literal -23 | -24 | t6 = tuple([1]) -25 | t7 = tuple((1,)) - - t8 = tuple([1,]) -26 + t8 = (1,) -27 | -28 | tuple([x for x in range(5)]) -29 | tuple({x for x in range(10)}) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list comprehension passed to `tuple()` (rewrite as a generator) - --> C409.py:28:1 - | -26 | t8 = tuple([1,]) -27 | -28 | tuple([x for x in range(5)]) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -29 | tuple({x for x in range(10)}) -30 | tuple(x for x in range(5)) - | -help: Rewrite as a generator -25 | t7 = tuple((1,)) -26 | t8 = tuple([1,]) -27 | - - tuple([x for x in range(5)]) -28 + tuple(x for x in range(5)) -29 | tuple({x for x in range(10)}) -30 | tuple(x for x in range(5)) -31 | tuple([ -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list comprehension passed to `tuple()` (rewrite as a generator) - --> C409.py:31:1 - | -29 | tuple({x for x in range(10)}) -30 | tuple(x for x in range(5)) -31 | / tuple([ -32 | | x for x in [1,2,3] -33 | | ]) - | |__^ -34 | tuple( # comment -35 | [x for x in [1,2,3]] - | -help: Rewrite as a generator -28 | tuple([x for x in range(5)]) -29 | tuple({x for x in range(10)}) -30 | tuple(x for x in range(5)) - - tuple([ - - x for x in [1,2,3] - - ]) -31 + tuple(x for x in [1,2,3]) -32 | tuple( # comment -33 | [x for x in [1,2,3]] -34 | ) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list comprehension passed to `tuple()` (rewrite as a generator) - --> C409.py:34:1 - | -32 | x for x in [1,2,3] -33 | ]) -34 | / tuple( # comment -35 | | [x for x in [1,2,3]] -36 | | ) - | |_^ -37 | tuple([ # comment -38 | x for x in range(10) - | -help: Rewrite as a generator -32 | x for x in [1,2,3] -33 | ]) -34 | tuple( # comment - - [x for x in [1,2,3]] -35 + x for x in [1,2,3] -36 | ) -37 | tuple([ # comment -38 | x for x in range(10) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list comprehension passed to `tuple()` (rewrite as a generator) - --> C409.py:37:1 - | -35 | [x for x in [1,2,3]] -36 | ) -37 | / tuple([ # comment -38 | | x for x in range(10) -39 | | ]) - | |__^ -40 | tuple( -41 | { - | -help: Rewrite as a generator -34 | tuple( # comment -35 | [x for x in [1,2,3]] -36 | ) - - tuple([ # comment - - x for x in range(10) - - ]) -37 | tuple( -38 + # comment -39 + x for x in range(10)) -40 + tuple( -41 | { -42 | x for x in [1,2,3] -43 | } -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:46:6 - | -44 | ) -45 | -46 | t9 = tuple([1],) - | ^^^^^^^^^^^ -47 | t10 = tuple([1, 2],) - | -help: Rewrite as a tuple literal -43 | } -44 | ) -45 | - - t9 = tuple([1],) -46 + t9 = (1,) -47 | t10 = tuple([1, 2],) -note: This is an unsafe fix and may change runtime behavior - -C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) - --> C409.py:47:7 - | -46 | t9 = tuple([1],) -47 | t10 = tuple([1, 2],) - | ^^^^^^^^^^^^^^ - | -help: Rewrite as a tuple literal -44 | ) -45 | -46 | t9 = tuple([1],) - - t10 = tuple([1, 2],) -47 + t10 = (1, 2) -note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap deleted file mode 100644 index 418db92027b68b..00000000000000 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py313.py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 0 diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py315.py.snap deleted file mode 100644 index a580bc898f2acb..00000000000000 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409_py315.py.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 1 - ---- Added --- -C409 Unnecessary list comprehension passed to `tuple()` (rewrite as a generator) - --> C409_py315.py:3:1 - | -1 | xs = [[1], [2]] -2 | -3 | tuple([*x for x in xs]) - | ^^^^^^^^^^^^^^^^^^^^^^^ - | -help: Rewrite as a generator From bf52caff35c2141f84a2a1038d116d46088bb55e Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 7 Jul 2026 12:40:39 -0400 Subject: [PATCH 3/5] Keep the Python 3.15 comprehension unpacking syntax test to test for "no change" in snapshot --- .../test/fixtures/flake8_comprehensions/C409_py315.py | 3 +++ crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs | 1 + ...les__flake8_comprehensions__tests__C409_C409_py315.py.snap | 4 ++++ 3 files changed, 8 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py create mode 100644 crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409_py315.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py new file mode 100644 index 00000000000000..c31654483296d7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C409_py315.py @@ -0,0 +1,3 @@ +xs = [[1], [2]] + +tuple([*x for x in xs]) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 4e128403c514db..200bbbfc526958 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -57,6 +57,7 @@ mod tests { #[test_case(Rule::UnnecessaryListComprehensionSet, Path::new("C403_py315.py"))] #[test_case(Rule::UnnecessaryListCall, Path::new("C411_py315.py"))] #[test_case(Rule::UnnecessaryLiteralWithinDictCall, Path::new("C418_py315.py"))] + #[test_case(Rule::UnnecessaryLiteralWithinTupleCall, Path::new("C409_py315.py"))] #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_py315.py"))] fn rules_py315(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409_py315.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409_py315.py.snap new file mode 100644 index 00000000000000..d9845b4ae92f9f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409_py315.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +--- + From 7dffce56d03a2dd6ef574d03331da5ef0d66b01b Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 7 Jul 2026 12:48:08 -0400 Subject: [PATCH 4/5] Fix cargo fmt and clippy --- crates/ruff_linter/src/checkers/ast/analyze/expression.rs | 4 +--- crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs | 2 +- .../rules/unnecessary_literal_within_tuple_call.rs | 6 +----- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 851d79547bcda2..04b7108a218b44 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -922,9 +922,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ); } if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinTupleCall) { - flake8_comprehensions::rules::unnecessary_literal_within_tuple_call( - checker, call, - ); + flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(checker, call); } if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinListCall) { flake8_comprehensions::rules::unnecessary_literal_within_list_call(checker, call); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 200bbbfc526958..49a5a754fe6e06 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -16,7 +16,7 @@ mod tests { use crate::settings::LinterSettings; use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_diagnostics, assert_diagnostics_diff}; + use crate::assert_diagnostics; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] #[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index e1872d1c1a562e..48b8e3e704ee0d 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -4,7 +4,6 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; -use crate::rules::flake8_comprehensions::fixes; use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::flake8_comprehensions::helpers; @@ -69,10 +68,7 @@ impl Violation for UnnecessaryLiteralWithinTupleCall { } /// C409 -pub(crate) fn unnecessary_literal_within_tuple_call( - checker: &Checker, - call: &ast::ExprCall, -) { +pub(crate) fn unnecessary_literal_within_tuple_call(checker: &Checker, call: &ast::ExprCall) { if !call.arguments.keywords.is_empty() { return; } From 8564ed362663bf5c134af367ab76c6baa4be6874 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 7 Jul 2026 13:04:14 -0400 Subject: [PATCH 5/5] Fix cargo fmt --- crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 49a5a754fe6e06..60e612f0578baa 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -12,11 +12,11 @@ mod tests { use ruff_python_ast::PythonVersion; use test_case::test_case; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::assert_diagnostics; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] #[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))]