From 9315ebfb5919e16c5a5f30ee45112e159a2506c7 Mon Sep 17 00:00:00 2001 From: Nate Bracy Date: Sun, 5 Jul 2026 23:52:13 -0400 Subject: [PATCH 1/2] Fix `ISC003` autofix incorrectly stripping `+` from comments --- .../flake8_implicit_str_concat/ISC.py | 17 +++++++++ .../rules/explicit.rs | 18 ++++++++- ...icit_str_concat__tests__ISC003_ISC.py.snap | 38 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) 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 871eaee71d523..042596f448b2b 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,20 @@ "abc" "def" ) + "ghi" + + +# https://github.com/astral-sh/ruff/issues/26541 +x = ( + "A" # Complains about this: + + + " broken" + + " line" + + " string." +) + +foo( + bar="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 95aaa1af82ca1..a822b4233b7dc 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; @@ -116,7 +116,21 @@ fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Fix { 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, after_plus) = if let Some(plus_token) = plus_token { + let plus_relative_start = plus_token.start() - left.end(); + let (before, after) = between_operands.split_at(plus_relative_start.to_usize()); + (before, &after[1..]) + } else { + // Fallback in case of unexpected token structure + between_operands.split_once('+').unwrap() + }; let linebreak_before_operator = before_plus.contains_line_break(TextRange::at(TextSize::new(0), before_plus.text_len())); 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 208fee3769386..5cba669de6a23 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,41 @@ 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" + | + +ISC003 [*] Explicitly concatenated string should be implicitly concatenated + --> ISC.py:236:9 + | +235 | foo( +236 | bar="A" # Complains about this: + + | _________^ +237 | | + " broken" + | |_______________^ +238 | + " line" +239 | + " string." + | +help: Remove redundant '+' operator to implicitly concatenate + | +236 | bar="A" # Complains about this: + + - + " broken" +237 + " broken" +238 | + " line" + | From e46715ca3d478b753088927fc0810270c6054ac4 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 6 Jul 2026 08:24:15 +0000 Subject: [PATCH 2/2] Refine ISC003 operator fix --- .../flake8_implicit_str_concat/ISC.py | 8 ------ .../rules/explicit.rs | 28 +++++++++---------- ...icit_str_concat__tests__ISC003_ISC.py.snap | 19 ------------- 3 files changed, 13 insertions(+), 42 deletions(-) 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 042596f448b2b..34753021ca4aa 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 @@ -231,11 +231,3 @@ + " line" + " string." ) - -foo( - bar="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 a822b4233b7dc..b9c8c6a7c3e6e 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 @@ -105,32 +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 plus_token = checker .tokens() .in_range(between_operands_range) .iter() - .find(|token| token.kind() == TokenKind::Plus); + .find(|token| token.kind() == TokenKind::Plus)?; - let (before_plus, after_plus) = if let Some(plus_token) = plus_token { - let plus_relative_start = plus_token.start() - left.end(); - let (before, after) = between_operands.split_at(plus_relative_start.to_usize()); - (before, &after[1..]) - } else { - // Fallback in case of unexpected token structure - between_operands.split_once('+').unwrap() - }; + 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())); @@ -143,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 5cba669de6a23..71bcd5365acd1 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 @@ -370,22 +370,3 @@ help: Remove redundant '+' operator to implicitly concatenate 230 + " broken" 231 | + " line" | - -ISC003 [*] Explicitly concatenated string should be implicitly concatenated - --> ISC.py:236:9 - | -235 | foo( -236 | bar="A" # Complains about this: + - | _________^ -237 | | + " broken" - | |_______________^ -238 | + " line" -239 | + " string." - | -help: Remove redundant '+' operator to implicitly concatenate - | -236 | bar="A" # Complains about this: + - - + " broken" -237 + " broken" -238 | + " line" - |