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 | ] + |