diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 4a333bdcd52065..54fe956d553094 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_index::Indexer; use ruff_python_parser::Tok; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::fix::edits::pad_start; @@ -25,23 +25,51 @@ use crate::fix::edits::pad_start; /// regex = r"\.png$" /// ``` /// +/// Or, if the string already contains a valid escape sequence: +/// ```python +/// value = "new line\nand invalid escape \_ here" +/// ``` +/// +/// Use instead: +/// ```python +/// value = "new line\nand invalid escape \\_ here" +/// ``` +/// /// ## References /// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[violation] -pub struct InvalidEscapeSequence(char); +pub struct InvalidEscapeSequence { + ch: char, + fix_title: FixTitle, +} impl AlwaysFixableViolation for InvalidEscapeSequence { #[derive_message_formats] fn message(&self) -> String { - let InvalidEscapeSequence(char) = self; - format!("Invalid escape sequence: `\\{char}`") + let InvalidEscapeSequence { ch, .. } = self; + format!("Invalid escape sequence: `\\{ch}`") } fn fix_title(&self) -> String { - "Add backslash to escape sequence".to_string() + match self.fix_title { + FixTitle::AddBackslash => format!("Add backslash to escape sequence"), + FixTitle::UseRawStringLiteral => format!("Use a raw string literal"), + } } } +#[derive(Debug, PartialEq, Eq)] +enum FixTitle { + AddBackslash, + UseRawStringLiteral, +} + +#[derive(Debug)] +struct InvalidEscapeChar { + ch: char, + range: TextRange, +} + /// W605 pub(crate) fn invalid_escape_sequence( diagnostics: &mut Vec, @@ -67,7 +95,7 @@ pub(crate) fn invalid_escape_sequence( }; let mut contains_valid_escape_sequence = false; - let mut invalid_escape_sequence = Vec::new(); + let mut invalid_escape_chars = Vec::new(); let mut prev = None; let bytes = token_source_code.as_bytes(); @@ -154,16 +182,28 @@ pub(crate) fn invalid_escape_sequence( let location = token_range.start() + TextSize::try_from(i).unwrap(); let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); - invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range)); + invalid_escape_chars.push(InvalidEscapeChar { + ch: next_char, + range, + }); } + let mut invalid_escape_sequence = Vec::new(); if contains_valid_escape_sequence { // Escape with backslash. - for diagnostic in &mut invalid_escape_sequence { - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + for invalid_escape_char in &invalid_escape_chars { + let diagnostic = Diagnostic::new( + InvalidEscapeSequence { + ch: invalid_escape_char.ch, + fix_title: FixTitle::AddBackslash, + }, + invalid_escape_char.range, + ) + .with_fix(Fix::safe_edit(Edit::insertion( r"\".to_string(), - diagnostic.start() + TextSize::from(1), + invalid_escape_char.range.start() + TextSize::from(1), ))); + invalid_escape_sequence.push(diagnostic); } } else { let tok_start = if token.is_f_string_middle() { @@ -178,14 +218,24 @@ pub(crate) fn invalid_escape_sequence( token_range.start() }; // Turn into raw string. - for diagnostic in &mut invalid_escape_sequence { - // If necessary, add a space between any leading keyword (`return`, `yield`, - // `assert`, etc.) and the string. For example, `return"foo"` is valid, but - // `returnr"foo"` is not. - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - pad_start("r".to_string(), tok_start, locator), - tok_start, - ))); + for invalid_escape_char in &invalid_escape_chars { + let diagnostic = Diagnostic::new( + InvalidEscapeSequence { + ch: invalid_escape_char.ch, + fix_title: FixTitle::UseRawStringLiteral, + }, + invalid_escape_char.range, + ) + .with_fix( + // If necessary, add a space between any leading keyword (`return`, `yield`, + // `assert`, etc.) and the string. For example, `return"foo"` is valid, but + // `returnr"foo"` is not. + Fix::safe_edit(Edit::insertion( + pad_start("r".to_string(), tok_start, locator), + tok_start, + )), + ); + invalid_escape_sequence.push(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap index c5997156d2aff6..b61df343bdcabc 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -9,7 +9,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.` 3 | 4 | #: W605:2:1 | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 1 1 | #: W605:1:10 @@ -27,7 +27,7 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.` | ^^ W605 7 | ''' | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 2 2 | regex = '\.png$' @@ -47,7 +47,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` | ^^ W605 12 | ) | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 8 8 | @@ -68,7 +68,7 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_` 19 | in the middle 20 | """ | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 12 12 | ) @@ -107,7 +107,7 @@ W605_0.py:28:12: W605 [*] Invalid escape sequence: `\.` 29 | 30 | #: Okay | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 25 25 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap index 5d7cd884cc6585..72749e97c1f843 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -9,7 +9,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.` 3 | 4 | #: W605:2:1 | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 1 1 | #: W605:1:10 @@ -27,7 +27,7 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.` | ^^ W605 7 | ''' | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 2 2 | regex = '\.png$' @@ -47,7 +47,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` | ^^ W605 12 | ) | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 8 8 | @@ -68,7 +68,7 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_` 19 | in the middle 20 | """ | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 12 12 | ) @@ -89,7 +89,7 @@ W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.` 26 | 27 | #: Okay | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 22 22 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_2.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_2.py.snap index c50eae3c81f98c..8d23dcae78eb14 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_2.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_2.py.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/src/rules/pycodestyle/mod.rs +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.` | @@ -9,7 +9,7 @@ W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.` 5 | 6 | #: W605:2:1 | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 1 1 | # Same as `W605_0.py` but using f-strings instead. @@ -29,7 +29,7 @@ W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.` | ^^ W605 9 | ''' | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 4 4 | regex = f'\.png$' @@ -49,7 +49,7 @@ W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_` | ^^ W605 14 | ) | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 10 10 | @@ -70,7 +70,7 @@ W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_` 21 | in the middle 22 | """ | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 14 14 | ) @@ -129,7 +129,7 @@ W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{` 45 | value = f'\{1}' 46 | value = f'{1:\}' | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 41 41 | ''' # noqa @@ -150,7 +150,7 @@ W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{` 46 | value = f'{1:\}' 47 | value = f"{f"\{1}"}" | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 42 42 | @@ -171,7 +171,7 @@ W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}` 47 | value = f"{f"\{1}"}" 48 | value = rf"{f"\{1}"}" | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 43 43 | regex = f'\\\_' @@ -191,7 +191,7 @@ W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{` | ^^ W605 48 | value = rf"{f"\{1}"}" | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 44 44 | value = f'\{{1}}' @@ -212,7 +212,7 @@ W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{` 49 | 50 | # Okay | - = help: Add backslash to escape sequence + = help: Use a raw string literal ℹ Fix 45 45 | value = f'\{1}'