diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py index 871eaee71d523b..34753021ca4aa2 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py @@ -222,3 +222,12 @@ "abc" "def" ) + "ghi" + + +# https://github.com/astral-sh/ruff/issues/26541 +x = ( + "A" # Complains about this: + + + " broken" + + " line" + + " string." +) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs index 95aaa1af82ca19..b9c8c6a7c3e6ed 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs @@ -1,5 +1,5 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::token::parenthesized_range; +use ruff_python_ast::token::{TokenKind, parenthesized_range}; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_trivia::is_python_whitespace; use ruff_source_file::LineRanges; @@ -105,18 +105,30 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) { return; } - diagnostic.set_fix(generate_fix(checker, bin_op)); + if let Some(fix) = generate_fix(checker, bin_op) { + diagnostic.set_fix(fix); + } } } } } -fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Fix { +fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Option { let ast::ExprBinOp { left, right, .. } = expr_bin_op; let between_operands_range = TextRange::new(left.end(), right.start()); - let between_operands = checker.locator().slice(between_operands_range); - let (before_plus, after_plus) = between_operands.split_once('+').unwrap(); + let plus_token = checker + .tokens() + .in_range(between_operands_range) + .iter() + .find(|token| token.kind() == TokenKind::Plus)?; + + let before_plus = checker + .locator() + .slice(TextRange::new(left.end(), plus_token.start())); + let after_plus = checker + .locator() + .slice(TextRange::new(plus_token.end(), right.start())); let linebreak_before_operator = before_plus.contains_line_break(TextRange::at(TextSize::new(0), before_plus.text_len())); @@ -129,8 +141,8 @@ fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Fix { before_plus.trim_end_matches(is_python_whitespace) }; - Fix::safe_edit(Edit::range_replacement( + Some(Fix::safe_edit(Edit::range_replacement( format!("{before_plus}{after_plus}"), between_operands_range, - )) + ))) } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap index 208fee37693866..71bcd5365acd1f 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap @@ -351,3 +351,22 @@ ISC003 Explicitly concatenated string should be implicitly concatenated | |_________^ | help: Remove redundant '+' operator to implicitly concatenate + +ISC003 [*] Explicitly concatenated string should be implicitly concatenated + --> ISC.py:229:5 + | +227 | # https://github.com/astral-sh/ruff/issues/26541 +228 | x = ( +229 | / "A" # Complains about this: + +230 | | + " broken" + | |_______________^ +231 | + " line" +232 | + " string." + | +help: Remove redundant '+' operator to implicitly concatenate + | +229 | "A" # Complains about this: + + - + " broken" +230 + " broken" +231 | + " line" + |