From e814283e086c2e8bcc7b89c625f8f46a97c6e14d Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 12 Dec 2025 16:43:34 -0800 Subject: [PATCH 01/10] wip --- .../flake8_implicit_str_concat/ISC004.py | 34 ++++++++ .../src/checkers/ast/analyze/expression.rs | 7 ++ crates/ruff_linter/src/codes.rs | 1 + .../rules/flake8_implicit_str_concat/mod.rs | 4 + .../rules/collection_literal.rs | 83 +++++++++++++++++++ .../flake8_implicit_str_concat/rules/mod.rs | 2 + ...t_str_concat__tests__ISC004_ISC004.py.snap | 24 ++++++ 7 files changed, 155 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py create mode 100644 crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs create mode 100644 crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py new file mode 100644 index 0000000000..f5299d0a1c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py @@ -0,0 +1,34 @@ +facts = ( + "Lobsters have blue blood.", + "The liver is the only human organ that can fully regenerate itself.", + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon.", +) + +facts = [ + "Lobsters have blue blood.", + "The liver is the only human organ that can fully regenerate itself.", + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon.", +] + +facts = ( + ( + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon." + ), +) + +facts = [ + ( + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon." + ), +] + +facts = ( + "Lobsters have blue blood.\n" + "The liver is the only human organ that can fully regenerate itself.\n" + "Clarinets are made almost entirely out of wood from the mpingo tree.\n" + "In 1971, astronaut Alan Shepard played golf on the moon.\n" +) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 53081e3681..21273d55c6 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -214,6 +214,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { range: _, node_index: _, }) => { + if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) { + flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal( + checker, + expr, + elts, + ); + } if ctx.is_store() { let check_too_many_expressions = checker.is_rule_enabled(Rule::ExpressionsInStarAssignment); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index ebec5f4acc..b005c6e914 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -454,6 +454,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation, (Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation, (Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation, + (Flake8ImplicitStrConcat, "004") => rules::flake8_implicit_str_concat::rules::ImplicitStringConcatenationInCollectionLiteral, // flake8-print (Flake8Print, "1") => rules::flake8_print::rules::Print, diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs index f02a049c5a..086ba8ce4c 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs @@ -32,6 +32,10 @@ mod tests { Path::new("ISC_syntax_error_2.py") )] #[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))] + #[test_case( + Rule::ImplicitStringConcatenationInCollectionLiteral, + Path::new("ISC004.py") + )] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs new file mode 100644 index 0000000000..da60f9c811 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -0,0 +1,83 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::token::parenthesized_range; +use ruff_python_ast::{Expr, StringLike}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::Violation; + +/// ## What it does +/// Checks for implicitly concatenated strings inside list and tuple literals. +/// +/// ## Why is this bad? +/// In collection literals, implicit string concatenation is often the result of +/// a missing comma between elements, which can silently merge items together. +/// +/// ## Example +/// ```python +/// facts = ( +/// "Lobsters have blue blood.", +/// "The liver is the only human organ that can fully regenerate itself.", +/// "Clarinets are made almost entirely out of wood from the mpingo tree." +/// "In 1971, astronaut Alan Shepard played golf on the moon.", +/// ) +/// ``` +/// +/// Use instead: +/// ```python +/// facts = ( +/// "Lobsters have blue blood.", +/// "The liver is the only human organ that can fully regenerate itself.", +/// "Clarinets are made almost entirely out of wood from the mpingo tree.", +/// "In 1971, astronaut Alan Shepard played golf on the moon.", +/// ) +/// ``` +/// +/// If the concatenation is intentional, wrap it in parentheses to make it +/// explicit: +/// ```python +/// facts = ( +/// "Lobsters have blue blood.", +/// "The liver is the only human organ that can fully regenerate itself.", +/// ( +/// "Clarinets are made almost entirely out of wood from the mpingo tree." +/// "In 1971, astronaut Alan Shepard played golf on the moon." +/// ), +/// ) +/// ``` +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "0.14.9")] +pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; + +impl Violation for ImplicitStringConcatenationInCollectionLiteral { + #[derive_message_formats] + fn message(&self) -> String { + "Implicit string concatenation in collection literal; did you forget a comma?".to_string() + } +} + +/// ISC004 +pub(crate) fn implicit_string_concatenation_in_collection_literal( + checker: &Checker, + expr: &Expr, + elements: &[Expr], +) { + for element in elements { + let Ok(string_like) = StringLike::try_from(element) else { + continue; + }; + if !string_like.is_implicit_concatenated() { + continue; + } + if parenthesized_range(string_like.as_expression_ref(), expr.into(), checker.tokens()) + .is_some() + { + continue; + } + + checker.report_diagnostic( + ImplicitStringConcatenationInCollectionLiteral, + string_like.range(), + ); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs index 8ec813567d..6bcbeeb2a2 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs @@ -1,5 +1,7 @@ pub(crate) use explicit::*; pub(crate) use implicit::*; +pub(crate) use collection_literal::*; +mod collection_literal; mod explicit; mod implicit; diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap new file mode 100644 index 0000000000..3e923c2845 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +--- +ISC004 Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:4:5 + | +2 | "Lobsters have blue blood.", +3 | "The liver is the only human organ that can fully regenerate itself.", +4 | / "Clarinets are made almost entirely out of wood from the mpingo tree." +5 | | "In 1971, astronaut Alan Shepard played golf on the moon.", + | |______________________________________________________________^ +6 | ) + | + +ISC004 Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:11:5 + | + 9 | "Lobsters have blue blood.", +10 | "The liver is the only human organ that can fully regenerate itself.", +11 | / "Clarinets are made almost entirely out of wood from the mpingo tree." +12 | | "In 1971, astronaut Alan Shepard played golf on the moon.", + | |______________________________________________________________^ +13 | ] + | From 148831199ce862444064d6922c1841fc4cb5493f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 12 Dec 2025 16:48:18 -0800 Subject: [PATCH 02/10] set --- .../fixtures/flake8_implicit_str_concat/ISC004.py | 14 ++++++++++++++ .../src/checkers/ast/analyze/expression.rs | 7 +++++++ .../rules/collection_literal.rs | 2 +- ...plicit_str_concat__tests__ISC004_ISC004.py.snap | 11 +++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py index f5299d0a1c..edd736f41b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py @@ -12,6 +12,20 @@ facts = [ "In 1971, astronaut Alan Shepard played golf on the moon.", ] +facts = { + "Lobsters have blue blood.", + "The liver is the only human organ that can fully regenerate itself.", + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon.", +} + +facts = { + ( + "Clarinets are made almost entirely out of wood from the mpingo tree." + "In 1971, astronaut Alan Shepard played golf on the moon." + ), +} + facts = ( ( "Clarinets are made almost entirely out of wood from the mpingo tree." diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 21273d55c6..0957aee346 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1336,6 +1336,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } Expr::Set(set) => { + if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) { + flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal( + checker, + expr, + &set.elts, + ); + } if checker.is_rule_enabled(Rule::DuplicateValue) { flake8_bugbear::rules::duplicate_value(checker, set); } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index da60f9c811..a3e8179f9d 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -7,7 +7,7 @@ use crate::checkers::ast::Checker; use crate::Violation; /// ## What it does -/// Checks for implicitly concatenated strings inside list and tuple literals. +/// Checks for implicitly concatenated strings inside list, tuple, and set literals. /// /// ## Why is this bad? /// In collection literals, implicit string concatenation is often the result of diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap index 3e923c2845..f54bea9e2a 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -22,3 +22,14 @@ ISC004 Implicit string concatenation in collection literal; did you forget a com | |______________________________________________________________^ 13 | ] | + +ISC004 Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:18:5 + | +16 | "Lobsters have blue blood.", +17 | "The liver is the only human organ that can fully regenerate itself.", +18 | / "Clarinets are made almost entirely out of wood from the mpingo tree." +19 | | "In 1971, astronaut Alan Shepard played golf on the moon.", + | |______________________________________________________________^ +20 | } + | From 46ce1efff351f991d8112e5a4a2a047f8fd5c95b Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 12 Dec 2025 17:18:15 -0800 Subject: [PATCH 03/10] comment --- .../flake8_implicit_str_concat/ISC004.py | 18 +++ .../rules/collection_literal.rs | 14 ++- ...t_str_concat__tests__ISC004_ISC004.py.snap | 108 +++++++++++++++++- 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py index edd736f41b..974667544a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py @@ -26,6 +26,24 @@ facts = { ), } +facts = ( + "Octopuses have three hearts." + # Missing comma here. + "Honey never spoils.", +) + +facts = [ + "Octopuses have three hearts." + # Missing comma here. + "Honey never spoils.", +] + +facts = { + "Octopuses have three hearts." + # Missing comma here. + "Honey never spoils.", +} + facts = ( ( "Clarinets are made almost entirely out of wood from the mpingo tree." diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index a3e8179f9d..db5449e824 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -4,7 +4,7 @@ use ruff_python_ast::{Expr, StringLike}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::Violation; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for implicitly concatenated strings inside list, tuple, and set literals. @@ -50,10 +50,16 @@ use crate::Violation; pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; impl Violation for ImplicitStringConcatenationInCollectionLiteral { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always; + #[derive_message_formats] fn message(&self) -> String { "Implicit string concatenation in collection literal; did you forget a comma?".to_string() } + + fn fix_title(&self) -> Option { + Some("Wrap implicitly concatenated strings in parentheses".to_string()) + } } /// ISC004 @@ -75,9 +81,13 @@ pub(crate) fn implicit_string_concatenation_in_collection_literal( continue; } - checker.report_diagnostic( + let mut diagnostic = checker.report_diagnostic( ImplicitStringConcatenationInCollectionLiteral, string_like.range(), ); + diagnostic.set_fix(Fix::safe_edits( + Edit::insertion("(".to_string(), string_like.range().start()), + [Edit::insertion(")".to_string(), string_like.range().end())], + )); } } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap index f54bea9e2a..cb063f8c7a 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC004 Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? --> ISC004.py:4:5 | 2 | "Lobsters have blue blood.", @@ -11,8 +11,19 @@ ISC004 Implicit string concatenation in collection literal; did you forget a com | |______________________________________________________________^ 6 | ) | +help: Wrap implicitly concatenated strings in parentheses +1 | facts = ( +2 | "Lobsters have blue blood.", +3 | "The liver is the only human organ that can fully regenerate itself.", + - "Clarinets are made almost entirely out of wood from the mpingo tree." + - "In 1971, astronaut Alan Shepard played golf on the moon.", +4 + ("Clarinets are made almost entirely out of wood from the mpingo tree." +5 + "In 1971, astronaut Alan Shepard played golf on the moon."), +6 | ) +7 | +8 | facts = [ -ISC004 Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? --> ISC004.py:11:5 | 9 | "Lobsters have blue blood.", @@ -22,8 +33,19 @@ ISC004 Implicit string concatenation in collection literal; did you forget a com | |______________________________________________________________^ 13 | ] | +help: Wrap implicitly concatenated strings in parentheses +8 | facts = [ +9 | "Lobsters have blue blood.", +10 | "The liver is the only human organ that can fully regenerate itself.", + - "Clarinets are made almost entirely out of wood from the mpingo tree." + - "In 1971, astronaut Alan Shepard played golf on the moon.", +11 + ("Clarinets are made almost entirely out of wood from the mpingo tree." +12 + "In 1971, astronaut Alan Shepard played golf on the moon."), +13 | ] +14 | +15 | facts = { -ISC004 Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? --> ISC004.py:18:5 | 16 | "Lobsters have blue blood.", @@ -33,3 +55,83 @@ ISC004 Implicit string concatenation in collection literal; did you forget a com | |______________________________________________________________^ 20 | } | +help: Wrap implicitly concatenated strings in parentheses +15 | facts = { +16 | "Lobsters have blue blood.", +17 | "The liver is the only human organ that can fully regenerate itself.", + - "Clarinets are made almost entirely out of wood from the mpingo tree." + - "In 1971, astronaut Alan Shepard played golf on the moon.", +18 + ("Clarinets are made almost entirely out of wood from the mpingo tree." +19 + "In 1971, astronaut Alan Shepard played golf on the moon."), +20 | } +21 | +22 | facts = { + +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:30:5 + | +29 | facts = ( +30 | / "Octopuses have three hearts." +31 | | # Missing comma here. +32 | | "Honey never spoils.", + | |_________________________^ +33 | ) + | +help: Wrap implicitly concatenated strings in parentheses +27 | } +28 | +29 | facts = ( + - "Octopuses have three hearts." +30 + ("Octopuses have three hearts." +31 | # Missing comma here. + - "Honey never spoils.", +32 + "Honey never spoils."), +33 | ) +34 | +35 | facts = [ + +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:36:5 + | +35 | facts = [ +36 | / "Octopuses have three hearts." +37 | | # Missing comma here. +38 | | "Honey never spoils.", + | |_________________________^ +39 | ] + | +help: Wrap implicitly concatenated strings in parentheses +33 | ) +34 | +35 | facts = [ + - "Octopuses have three hearts." +36 + ("Octopuses have three hearts." +37 | # Missing comma here. + - "Honey never spoils.", +38 + "Honey never spoils."), +39 | ] +40 | +41 | facts = { + +ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? + --> ISC004.py:42:5 + | +41 | facts = { +42 | / "Octopuses have three hearts." +43 | | # Missing comma here. +44 | | "Honey never spoils.", + | |_________________________^ +45 | } + | +help: Wrap implicitly concatenated strings in parentheses +39 | ] +40 | +41 | facts = { + - "Octopuses have three hearts." +42 + ("Octopuses have three hearts." +43 | # Missing comma here. + - "Honey never spoils.", +44 + "Honey never spoils."), +45 | } +46 | +47 | facts = ( From aa9c9dcce1d156e664cb3bf767f1c9c65438fa49 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 13 Dec 2025 19:12:04 -0800 Subject: [PATCH 04/10] doc --- .../flake8_implicit_str_concat/rules/collection_literal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index db5449e824..6c76791f2f 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -23,7 +23,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ) /// ``` /// -/// Use instead: +/// Instead, you likely intended: /// ```python /// facts = ( /// "Lobsters have blue blood.", From 55442384ee13d8b14dbb5a55fa1bf3329af610e1 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 13 Dec 2025 19:25:45 -0800 Subject: [PATCH 05/10] gen all --- ruff.schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.schema.json b/ruff.schema.json index 1c8a092042..0dd54c0a07 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3482,6 +3482,7 @@ "ISC001", "ISC002", "ISC003", + "ISC004", "LOG", "LOG0", "LOG00", From 986834947530d9cb45c37f9bfa433263bb0e038c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 13 Dec 2025 22:21:24 -0800 Subject: [PATCH 06/10] ci fixes --- _typos.toml | 1 + .../rules/collection_literal.rs | 8 ++++++-- .../src/rules/flake8_implicit_str_concat/rules/mod.rs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/_typos.toml b/_typos.toml index c4ff9f6151..11c3cad523 100644 --- a/_typos.toml +++ b/_typos.toml @@ -4,6 +4,7 @@ extend-exclude = [ "crates/ty_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*", + "crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs", # Completion tests tend to have a lot of incomplete # words naturally. It's annoying to have to make all # of them actually words. So just ignore typos here. diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index 6c76791f2f..df438e693c 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -75,8 +75,12 @@ pub(crate) fn implicit_string_concatenation_in_collection_literal( if !string_like.is_implicit_concatenated() { continue; } - if parenthesized_range(string_like.as_expression_ref(), expr.into(), checker.tokens()) - .is_some() + if parenthesized_range( + string_like.as_expression_ref(), + expr.into(), + checker.tokens(), + ) + .is_some() { continue; } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs index 6bcbeeb2a2..bbcc443a24 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/mod.rs @@ -1,6 +1,6 @@ +pub(crate) use collection_literal::*; pub(crate) use explicit::*; pub(crate) use implicit::*; -pub(crate) use collection_literal::*; mod collection_literal; mod explicit; From cce85bc95e3a973505989b1824b25d5e95c636a3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 13 Dec 2025 23:24:54 -0800 Subject: [PATCH 07/10] error text --- .../rules/collection_literal.rs | 3 ++- ...implicit_str_concat__tests__ISC004_ISC004.py.snap | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index df438e693c..83f669c98b 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -54,7 +54,8 @@ impl Violation for ImplicitStringConcatenationInCollectionLiteral { #[derive_message_formats] fn message(&self) -> String { - "Implicit string concatenation in collection literal; did you forget a comma?".to_string() + "Unparenthesized implicit string concatenation in collection; did you forget a comma?" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap index cb063f8c7a..2a7c6947c2 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:4:5 | 2 | "Lobsters have blue blood.", @@ -23,7 +23,7 @@ help: Wrap implicitly concatenated strings in parentheses 7 | 8 | facts = [ -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:11:5 | 9 | "Lobsters have blue blood.", @@ -45,7 +45,7 @@ help: Wrap implicitly concatenated strings in parentheses 14 | 15 | facts = { -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:18:5 | 16 | "Lobsters have blue blood.", @@ -67,7 +67,7 @@ help: Wrap implicitly concatenated strings in parentheses 21 | 22 | facts = { -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:30:5 | 29 | facts = ( @@ -90,7 +90,7 @@ help: Wrap implicitly concatenated strings in parentheses 34 | 35 | facts = [ -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:36:5 | 35 | facts = [ @@ -113,7 +113,7 @@ help: Wrap implicitly concatenated strings in parentheses 40 | 41 | facts = { -ISC004 [*] Implicit string concatenation in collection literal; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? --> ISC004.py:42:5 | 41 | facts = { From 723f590ea41f87caa46c69b519ff6ba9805fda87 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 16 Dec 2025 12:58:00 -0800 Subject: [PATCH 08/10] follow up --- .../rules/collection_literal.rs | 10 ++++++-- ...t_str_concat__tests__ISC004_ISC004.py.snap | 24 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index 83f669c98b..c1b5d96058 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -45,6 +45,11 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ), /// ) /// ``` +/// +/// ## Fix safety +/// The fix is safe in that it does not change the semantics of your code. +/// However, the issue is that you may often want to change semantics +/// by adding a missing comma. #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.14.9")] pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; @@ -54,7 +59,7 @@ impl Violation for ImplicitStringConcatenationInCollectionLiteral { #[derive_message_formats] fn message(&self) -> String { - "Unparenthesized implicit string concatenation in collection; did you forget a comma?" + "Unparenthesized implicit string concatenation in collection" .to_string() } @@ -90,7 +95,8 @@ pub(crate) fn implicit_string_concatenation_in_collection_literal( ImplicitStringConcatenationInCollectionLiteral, string_like.range(), ); - diagnostic.set_fix(Fix::safe_edits( + diagnostic.help("Did you forget a comma?"); + diagnostic.set_fix(Fix::unsafe_edits( Edit::insertion("(".to_string(), string_like.range().start()), [Edit::insertion(")".to_string(), string_like.range().end())], )); diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap index 2a7c6947c2..e2b954b79f 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:4:5 | 2 | "Lobsters have blue blood.", @@ -12,6 +12,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 6 | ) | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 1 | facts = ( 2 | "Lobsters have blue blood.", 3 | "The liver is the only human organ that can fully regenerate itself.", @@ -22,8 +23,9 @@ help: Wrap implicitly concatenated strings in parentheses 6 | ) 7 | 8 | facts = [ +note: This is an unsafe fix and may change runtime behavior -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:11:5 | 9 | "Lobsters have blue blood.", @@ -34,6 +36,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 13 | ] | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 8 | facts = [ 9 | "Lobsters have blue blood.", 10 | "The liver is the only human organ that can fully regenerate itself.", @@ -44,8 +47,9 @@ help: Wrap implicitly concatenated strings in parentheses 13 | ] 14 | 15 | facts = { +note: This is an unsafe fix and may change runtime behavior -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:18:5 | 16 | "Lobsters have blue blood.", @@ -56,6 +60,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 20 | } | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 15 | facts = { 16 | "Lobsters have blue blood.", 17 | "The liver is the only human organ that can fully regenerate itself.", @@ -66,8 +71,9 @@ help: Wrap implicitly concatenated strings in parentheses 20 | } 21 | 22 | facts = { +note: This is an unsafe fix and may change runtime behavior -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:30:5 | 29 | facts = ( @@ -78,6 +84,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 33 | ) | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 27 | } 28 | 29 | facts = ( @@ -89,8 +96,9 @@ help: Wrap implicitly concatenated strings in parentheses 33 | ) 34 | 35 | facts = [ +note: This is an unsafe fix and may change runtime behavior -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:36:5 | 35 | facts = [ @@ -101,6 +109,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 39 | ] | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 33 | ) 34 | 35 | facts = [ @@ -112,8 +121,9 @@ help: Wrap implicitly concatenated strings in parentheses 39 | ] 40 | 41 | facts = { +note: This is an unsafe fix and may change runtime behavior -ISC004 [*] Unparenthesized implicit string concatenation in collection; did you forget a comma? +ISC004 [*] Unparenthesized implicit string concatenation in collection --> ISC004.py:42:5 | 41 | facts = { @@ -124,6 +134,7 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection; did you 45 | } | help: Wrap implicitly concatenated strings in parentheses +help: Did you forget a comma? 39 | ] 40 | 41 | facts = { @@ -135,3 +146,4 @@ help: Wrap implicitly concatenated strings in parentheses 45 | } 46 | 47 | facts = ( +note: This is an unsafe fix and may change runtime behavior From 16cb4ccf61607934dba93f415adf406b7b0a05b3 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 16 Dec 2025 13:02:54 -0800 Subject: [PATCH 09/10] fmt --- .../flake8_implicit_str_concat/rules/collection_literal.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index c1b5d96058..c42b590472 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -59,8 +59,7 @@ impl Violation for ImplicitStringConcatenationInCollectionLiteral { #[derive_message_formats] fn message(&self) -> String { - "Unparenthesized implicit string concatenation in collection" - .to_string() + "Unparenthesized implicit string concatenation in collection".to_string() } fn fix_title(&self) -> Option { From c01733286765d0bc8da3bd0481e612817b9723bf Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 16 Dec 2025 13:26:36 -0800 Subject: [PATCH 10/10] more ruff more version --- .../flake8_implicit_str_concat/rules/collection_literal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index c42b590472..b82072dcac 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -51,7 +51,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// However, the issue is that you may often want to change semantics /// by adding a missing comma. #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.9")] +#[violation_metadata(preview_since = "0.14.10")] pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; impl Violation for ImplicitStringConcatenationInCollectionLiteral {