From e2130707f58765d8caade71c6dabb94bbb9dbfba Mon Sep 17 00:00:00 2001 From: Timofei Kukushkin Date: Tue, 13 Jun 2023 03:28:57 +0400 Subject: [PATCH] Autofixer for ISC001 (#4853) ## Summary This PR adds autofixer for rule ISC001 in cases where both string literals are of the same kind and with same quotes (double / single). Fixes #4829 ## Test Plan I added testcases with different combinations of string literals. --- .../flake8_implicit_str_concat/ISC.py | 16 +++ .../rules/implicit.rs | 55 ++++++- ...icit_str_concat__tests__ISC001_ISC.py.snap | 135 +++++++++++++++++- ...oncat__tests__multiline_ISC001_ISC.py.snap | 135 +++++++++++++++++- 4 files changed, 332 insertions(+), 9 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py index a88e8e5573..633e075731 100644 --- a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py +++ b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py @@ -34,3 +34,19 @@ _ = ( b"abc" b"def" ) + +_ = """a""" """b""" + +_ = """a +b""" """c +d""" + +_ = f"""a""" f"""b""" + +_ = f"a" "b" + +_ = """a""" "b" + +_ = 'a' "b" + +_ = rf"a" rf"b" diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 43dac7b3e5..63e107e3bb 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -3,9 +3,10 @@ use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::str::{leading_quote, trailing_quote}; use crate::rules::flake8_implicit_str_concat::settings::Settings; @@ -34,10 +35,16 @@ use crate::rules::flake8_implicit_str_concat::settings::Settings; pub struct SingleLineImplicitStringConcatenation; impl Violation for SingleLineImplicitStringConcatenation { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Implicitly concatenated string literals on one line") } + + fn autofix_title(&self) -> Option { + Some("Combine string literals".to_string()) + } } /// ## What it does @@ -106,12 +113,50 @@ pub(crate) fn implicit( TextRange::new(a_range.start(), b_range.end()), )); } else { - diagnostics.push(Diagnostic::new( + let mut diagnostic = Diagnostic::new( SingleLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), - )); - } - } + ); + + if let Some(fix) = concatenate_strings(*a_range, *b_range, locator) { + diagnostic.set_fix(fix); + } + + diagnostics.push(diagnostic); + }; + }; } diagnostics } + +fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option { + let a_text = &locator.contents()[a_range]; + let b_text = &locator.contents()[b_range]; + + let a_leading_quote = leading_quote(a_text)?; + let b_leading_quote = leading_quote(b_text)?; + + // Require, for now, that the leading quotes are the same. + if a_leading_quote != b_leading_quote { + return None; + } + + let a_trailing_quote = trailing_quote(a_text)?; + let b_trailing_quote = trailing_quote(b_text)?; + + // Require, for now, that the trailing quotes are the same. + if a_trailing_quote != b_trailing_quote { + return None; + } + + let a_body = &a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]; + let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()]; + + let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}"); + let range = TextRange::new(a_range.start(), b_range.end()); + + Some(Fix::automatic(Edit::range_replacement( + concatenation, + range, + ))) +} diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab" diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab"