From 16060670b85d6a271894ae95e21e73e272cddaee Mon Sep 17 00:00:00 2001 From: Adrian Date: Mon, 13 Nov 2023 22:59:37 +0100 Subject: [PATCH] Add new rule to check for useless quote escapes (#8630) When using the autofixer for `Q000` it does not remove the backslashes from quotes that no longer need escaping. This new rule checks for such backslashes (regardless whether they come from the autofixer or not) and can remove them. fixes #8617 --- .../doubles_escaped_unnecessary.py | 45 +++ .../singles_escaped_unnecessary.py | 43 ++ crates/ruff_linter/src/checkers/tokens.rs | 4 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_quotes/mod.rs | 6 + .../rules/avoidable_escaped_quote.rs | 243 ++++++++++- ...s_over_singles_escaped_unnecessary.py.snap | 341 ++++++++++++++++ ...s_over_doubles_escaped_unnecessary.py.snap | 382 ++++++++++++++++++ ruff.schema.json | 1 + scripts/check_docs_formatted.py | 1 + 10 files changed, 1048 insertions(+), 19 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped_unnecessary.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped_unnecessary.py create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped_unnecessary.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped_unnecessary.py new file mode 100644 index 0000000000..7b7b4b7acc --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped_unnecessary.py @@ -0,0 +1,45 @@ +this_should_raise_Q004 = 'This is a \"string\"' +this_should_raise_Q004 = 'This is \\ a \\\"string\"' +this_is_fine = '"This" is a \"string\"' +this_is_fine = "This is a 'string'" +this_is_fine = "\"This\" is a 'string'" +this_is_fine = r'This is a \"string\"' +this_is_fine = R'This is a \"string\"' +this_should_raise_Q004 = ( + 'This is a' + '\"string\"' +) + +# Same as above, but with f-strings +f'This is a \"string\"' # Q004 +f'This is \\ a \\\"string\"' # Q004 +f'"This" is a \"string\"' +f"This is a 'string'" +f"\"This\" is a 'string'" +fr'This is a \"string\"' +fR'This is a \"string\"' +this_should_raise_Q004 = ( + f'This is a' + f'\"string\"' # Q004 +) + +# Nested f-strings (Python 3.12+) +# +# The first one is interesting because the fix for it is valid pre 3.12: +# +# f"'foo' {'nested'}" +# +# but as the actual string itself is invalid pre 3.12, we don't catch it. +f'\"foo\" {'nested'}' # Q004 +f'\"foo\" {f'nested'}' # Q004 +f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + +f'normal {f'nested'} normal' +f'\"normal\" {f'nested'} normal' # Q004 +f'\"normal\" {f'nested'} "double quotes"' +f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + +# Make sure we do not unescape quotes +this_is_fine = 'This is an \\"escaped\\" quote' +this_should_raise_Q004 = 'This is an \\\"escaped\\\" quote with an extra backslash' diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped_unnecessary.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped_unnecessary.py new file mode 100644 index 0000000000..63fe16806a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped_unnecessary.py @@ -0,0 +1,43 @@ +this_should_raise_Q004 = "This is a \'string\'" +this_should_raise_Q004 = "'This' is a \'string\'" +this_is_fine = 'This is a "string"' +this_is_fine = '\'This\' is a "string"' +this_is_fine = r"This is a \'string\'" +this_is_fine = R"This is a \'string\'" +this_should_raise_Q004 = ( + "This is a" + "\'string\'" +) + +# Same as above, but with f-strings +f"This is a \'string\'" # Q004 +f"'This' is a \'string\'" # Q004 +f'This is a "string"' +f'\'This\' is a "string"' +fr"This is a \'string\'" +fR"This is a \'string\'" +this_should_raise_Q004 = ( + f"This is a" + f"\'string\'" # Q004 +) + +# Nested f-strings (Python 3.12+) +# +# The first one is interesting because the fix for it is valid pre 3.12: +# +# f'"foo" {"nested"}' +# +# but as the actual string itself is invalid pre 3.12, we don't catch it. +f"\'foo\' {"foo"}" # Q004 +f"\'foo\' {f"foo"}" # Q004 +f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + +f"normal {f"nested"} normal" +f"\'normal\' {f"nested"} normal" # Q004 +f"\'normal\' {f"nested"} 'single quotes'" +f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + +# Make sure we do not unescape quotes +this_is_fine = "This is an \\'escaped\\' quote" +this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index 900f75eb27..9f3f0866f7 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -115,6 +115,10 @@ pub(crate) fn check_tokens( flake8_quotes::rules::avoidable_escaped_quote(&mut diagnostics, tokens, locator, settings); } + if settings.rules.enabled(Rule::UnnecessaryEscapedQuote) { + flake8_quotes::rules::unnecessary_escaped_quote(&mut diagnostics, tokens, locator); + } + if settings.rules.any_enabled(&[ Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 8efb7b5e2c..546babe48e 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -403,6 +403,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Quotes, "001") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesMultilineString), (Flake8Quotes, "002") => (RuleGroup::Stable, rules::flake8_quotes::rules::BadQuotesDocstring), (Flake8Quotes, "003") => (RuleGroup::Stable, rules::flake8_quotes::rules::AvoidableEscapedQuote), + (Flake8Quotes, "004") => (RuleGroup::Preview, rules::flake8_quotes::rules::UnnecessaryEscapedQuote), // flake8-annotations (Flake8Annotations, "001") => (RuleGroup::Stable, rules::flake8_annotations::rules::MissingTypeFunctionArgument), diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 1d178d1f14..07ede87903 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -19,6 +19,7 @@ mod tests { #[test_case(Path::new("doubles.py"))] #[test_case(Path::new("doubles_escaped.py"))] + #[test_case(Path::new("doubles_escaped_unnecessary.py"))] #[test_case(Path::new("doubles_implicit.py"))] #[test_case(Path::new("doubles_multiline_string.py"))] #[test_case(Path::new("doubles_noqa.py"))] @@ -39,6 +40,7 @@ mod tests { Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, + Rule::UnnecessaryEscapedQuote, ]) }, )?; @@ -86,6 +88,7 @@ mod tests { #[test_case(Path::new("singles.py"))] #[test_case(Path::new("singles_escaped.py"))] + #[test_case(Path::new("singles_escaped_unnecessary.py"))] #[test_case(Path::new("singles_implicit.py"))] #[test_case(Path::new("singles_multiline_string.py"))] #[test_case(Path::new("singles_noqa.py"))] @@ -106,6 +109,7 @@ mod tests { Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, + Rule::UnnecessaryEscapedQuote, ]) }, )?; @@ -139,6 +143,7 @@ mod tests { Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, + Rule::UnnecessaryEscapedQuote, ]) }, )?; @@ -172,6 +177,7 @@ mod tests { Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, + Rule::UnnecessaryEscapedQuote, ]) }, )?; diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index ab4f2ef42e..95b9b25366 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -7,9 +7,10 @@ use ruff_source_file::Locator; use ruff_text_size::TextRange; use crate::lex::docstring_detection::StateMachine; - use crate::settings::LinterSettings; +use super::super::settings::Quote; + /// ## What it does /// Checks for strings that include escaped quotes, and suggests changing /// the quote style to avoid the need to escape them. @@ -48,6 +49,43 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote { } } +/// ## What it does +/// Checks for strings that include unnecessarily escaped quotes. +/// +/// ## Why is this bad? +/// If a string contains an escaped quote that doesn't match the quote +/// character used for the string, it's unnecessary and can be removed. +/// +/// ## Example +/// ```python +/// foo = "bar\'s" +/// ``` +/// +/// Use instead: +/// ```python +/// foo = "bar's" +/// ``` +/// +/// ## Formatter compatibility +/// We recommend against using this rule alongside the [formatter]. The +/// formatter automatically removes unnecessary escapes, making the rule +/// redundant. +/// +/// [formatter]: https://docs.astral.sh/ruff/formatter +#[violation] +pub struct UnnecessaryEscapedQuote; + +impl AlwaysFixableViolation for UnnecessaryEscapedQuote { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unnecessary escape on inner quote character") + } + + fn fix_title(&self) -> String { + "Remove backslash".to_string() + } +} + struct FStringContext { /// Whether to check for escaped quotes in the f-string. check_for_escaped_quote: bool, @@ -55,14 +93,21 @@ struct FStringContext { start_range: TextRange, /// The ranges of the f-string middle tokens containing escaped quotes. middle_ranges_with_escapes: Vec, + /// The quote style used for the f-string + quote_style: Quote, } impl FStringContext { - fn new(check_for_escaped_quote: bool, fstring_start_range: TextRange) -> Self { + fn new( + check_for_escaped_quote: bool, + fstring_start_range: TextRange, + quote_style: Quote, + ) -> Self { Self { check_for_escaped_quote, start_range: fstring_start_range, middle_ranges_with_escapes: vec![], + quote_style, } } @@ -132,21 +177,27 @@ pub(crate) fn avoidable_escaped_quote( } // Check if we're using the preferred quotation style. - if !leading_quote(locator.slice(tok_range)) - .is_some_and(|text| text.contains(quotes_settings.inline_quotes.as_char())) - { + if !leading_quote(locator.slice(tok_range)).is_some_and(|text| { + contains_quote(text, quotes_settings.inline_quotes.as_char()) + }) { continue; } - if string_contents.contains(quotes_settings.inline_quotes.as_char()) - && !string_contents.contains(quotes_settings.inline_quotes.opposite().as_char()) + if contains_escaped_quote(string_contents, quotes_settings.inline_quotes.as_char()) + && !contains_quote( + string_contents, + quotes_settings.inline_quotes.opposite().as_char(), + ) { let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, tok_range); let fixed_contents = format!( "{prefix}{quote}{value}{quote}", prefix = kind.as_str(), quote = quotes_settings.inline_quotes.opposite().as_char(), - value = unescape_string(string_contents) + value = unescape_string( + string_contents, + quotes_settings.inline_quotes.as_char() + ) ); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( fixed_contents, @@ -159,10 +210,13 @@ pub(crate) fn avoidable_escaped_quote( let text = locator.slice(tok_range); // Check for escaped quote only if we're using the preferred quotation // style and it isn't a triple-quoted f-string. - let check_for_escaped_quote = text - .contains(quotes_settings.inline_quotes.as_char()) - && !is_triple_quote(text); - fstrings.push(FStringContext::new(check_for_escaped_quote, tok_range)); + let check_for_escaped_quote = !is_triple_quote(text) + && contains_quote(text, quotes_settings.inline_quotes.as_char()); + fstrings.push(FStringContext::new( + check_for_escaped_quote, + tok_range, + quotes_settings.inline_quotes, + )); } Tok::FStringMiddle { value: string_contents, @@ -176,11 +230,15 @@ pub(crate) fn avoidable_escaped_quote( } // If any part of the f-string contains the opposite quote, // we can't change the quote style in the entire f-string. - if string_contents.contains(quotes_settings.inline_quotes.opposite().as_char()) { + if contains_quote( + string_contents, + quotes_settings.inline_quotes.opposite().as_char(), + ) { context.ignore_escaped_quotes(); continue; } - if string_contents.contains(quotes_settings.inline_quotes.as_char()) { + if contains_escaped_quote(string_contents, quotes_settings.inline_quotes.as_char()) + { context.push_fstring_middle_range(tok_range); } } @@ -207,7 +265,13 @@ pub(crate) fn avoidable_escaped_quote( .middle_ranges_with_escapes .iter() .map(|&range| { - Edit::range_replacement(unescape_string(locator.slice(range)), range) + Edit::range_replacement( + unescape_string( + locator.slice(range), + quotes_settings.inline_quotes.as_char(), + ), + range, + ) }) .chain(std::iter::once( // `FStringEnd` edit @@ -231,13 +295,152 @@ pub(crate) fn avoidable_escaped_quote( } } -fn unescape_string(value: &str) -> String { - let mut fixed_contents = String::with_capacity(value.len()); +/// Q004 +pub(crate) fn unnecessary_escaped_quote( + diagnostics: &mut Vec, + lxr: &[LexResult], + locator: &Locator, +) { + let mut fstrings: Vec = Vec::new(); + let mut state_machine = StateMachine::default(); - let mut chars = value.chars().peekable(); + for &(ref tok, tok_range) in lxr.iter().flatten() { + let is_docstring = state_machine.consume(tok); + if is_docstring { + continue; + } + + match tok { + Tok::String { + value: string_contents, + kind, + triple_quoted, + } => { + if kind.is_raw() || *triple_quoted { + continue; + } + + let leading = match leading_quote(locator.slice(tok_range)) { + Some("\"") => Quote::Double, + Some("'") => Quote::Single, + _ => continue, + }; + if !contains_escaped_quote(string_contents, leading.opposite().as_char()) { + continue; + } + + let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, tok_range); + let fixed_contents = format!( + "{prefix}{quote}{value}{quote}", + prefix = kind.as_str(), + quote = leading.as_char(), + value = unescape_string(string_contents, leading.opposite().as_char()) + ); + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + fixed_contents, + tok_range, + ))); + diagnostics.push(diagnostic); + } + Tok::FStringStart => { + let text = locator.slice(tok_range); + // Check for escaped quote only if we're using the preferred quotation + // style and it isn't a triple-quoted f-string. + let check_for_escaped_quote = !is_triple_quote(text); + let quote_style = if contains_quote(text, Quote::Single.as_char()) { + Quote::Single + } else { + Quote::Double + }; + fstrings.push(FStringContext::new( + check_for_escaped_quote, + tok_range, + quote_style, + )); + } + Tok::FStringMiddle { + value: string_contents, + is_raw, + } if !is_raw => { + let Some(context) = fstrings.last_mut() else { + continue; + }; + if !context.check_for_escaped_quote { + continue; + } + if contains_escaped_quote(string_contents, context.quote_style.opposite().as_char()) + { + context.push_fstring_middle_range(tok_range); + } + } + Tok::FStringEnd => { + let Some(context) = fstrings.pop() else { + continue; + }; + let [first, rest @ ..] = context.middle_ranges_with_escapes.as_slice() else { + continue; + }; + let mut diagnostic = Diagnostic::new( + UnnecessaryEscapedQuote, + TextRange::new(context.start_range.start(), tok_range.end()), + ); + let first_edit = Edit::range_replacement( + unescape_string( + locator.slice(first), + context.quote_style.opposite().as_char(), + ), + *first, + ); + let rest_edits = rest.iter().map(|&range| { + Edit::range_replacement( + unescape_string( + locator.slice(range), + context.quote_style.opposite().as_char(), + ), + range, + ) + }); + diagnostic.set_fix(Fix::safe_edits(first_edit, rest_edits)); + diagnostics.push(diagnostic); + } + _ => {} + } + } +} + +/// Return `true` if the haystack contains the quote. +fn contains_quote(haystack: &str, quote: char) -> bool { + memchr::memchr(quote as u8, haystack.as_bytes()).is_some() +} + +/// Return `true` if the haystack contains an escaped quote. +fn contains_escaped_quote(haystack: &str, quote: char) -> bool { + for index in memchr::memchr_iter(quote as u8, haystack.as_bytes()) { + // If the quote is preceded by an even number of backslashes, it's not escaped. + if haystack.as_bytes()[..index] + .iter() + .rev() + .take_while(|&&c| c == b'\\') + .count() + % 2 + != 0 + { + return true; + } + } + false +} + +/// Return a modified version of the string with all quote escapes removed. +fn unescape_string(haystack: &str, quote: char) -> String { + let mut fixed_contents = String::with_capacity(haystack.len()); + + let mut chars = haystack.chars().peekable(); + let mut backslashes = 0; while let Some(char) = chars.next() { if char != '\\' { fixed_contents.push(char); + backslashes = 0; continue; } // If we're at the end of the line @@ -246,9 +449,11 @@ fn unescape_string(value: &str) -> String { continue; }; // Remove quote escape - if matches!(*next_char, '\'' | '"') { + if *next_char == quote && backslashes % 2 == 0 { + backslashes = 0; continue; } + backslashes += 1; fixed_contents.push(char); } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap new file mode 100644 index 0000000000..e0114d12ab --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_unnecessary.py.snap @@ -0,0 +1,341 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +singles_escaped_unnecessary.py:1:26: Q004 [*] Unnecessary escape on inner quote character + | +1 | this_should_raise_Q004 = "This is a \'string\'" + | ^^^^^^^^^^^^^^^^^^^^^^ Q004 +2 | this_should_raise_Q004 = "'This' is a \'string\'" +3 | this_is_fine = 'This is a "string"' + | + = help: Remove backslash + +ℹ Safe fix +1 |-this_should_raise_Q004 = "This is a \'string\'" + 1 |+this_should_raise_Q004 = "This is a 'string'" +2 2 | this_should_raise_Q004 = "'This' is a \'string\'" +3 3 | this_is_fine = 'This is a "string"' +4 4 | this_is_fine = '\'This\' is a "string"' + +singles_escaped_unnecessary.py:2:26: Q004 [*] Unnecessary escape on inner quote character + | +1 | this_should_raise_Q004 = "This is a \'string\'" +2 | this_should_raise_Q004 = "'This' is a \'string\'" + | ^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +3 | this_is_fine = 'This is a "string"' +4 | this_is_fine = '\'This\' is a "string"' + | + = help: Remove backslash + +ℹ Safe fix +1 1 | this_should_raise_Q004 = "This is a \'string\'" +2 |-this_should_raise_Q004 = "'This' is a \'string\'" + 2 |+this_should_raise_Q004 = "'This' is a 'string'" +3 3 | this_is_fine = 'This is a "string"' +4 4 | this_is_fine = '\'This\' is a "string"' +5 5 | this_is_fine = r"This is a \'string\'" + +singles_escaped_unnecessary.py:9:5: Q004 [*] Unnecessary escape on inner quote character + | + 7 | this_should_raise_Q004 = ( + 8 | "This is a" + 9 | "\'string\'" + | ^^^^^^^^^^^^ Q004 +10 | ) + | + = help: Remove backslash + +ℹ Safe fix +6 6 | this_is_fine = R"This is a \'string\'" +7 7 | this_should_raise_Q004 = ( +8 8 | "This is a" +9 |- "\'string\'" + 9 |+ "'string'" +10 10 | ) +11 11 | +12 12 | # Same as above, but with f-strings + +singles_escaped_unnecessary.py:13:1: Q004 [*] Unnecessary escape on inner quote character + | +12 | # Same as above, but with f-strings +13 | f"This is a \'string\'" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q004 +14 | f"'This' is a \'string\'" # Q004 +15 | f'This is a "string"' + | + = help: Remove backslash + +ℹ Safe fix +10 10 | ) +11 11 | +12 12 | # Same as above, but with f-strings +13 |-f"This is a \'string\'" # Q004 + 13 |+f"This is a 'string'" # Q004 +14 14 | f"'This' is a \'string\'" # Q004 +15 15 | f'This is a "string"' +16 16 | f'\'This\' is a "string"' + +singles_escaped_unnecessary.py:14:1: Q004 [*] Unnecessary escape on inner quote character + | +12 | # Same as above, but with f-strings +13 | f"This is a \'string\'" # Q004 +14 | f"'This' is a \'string\'" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +15 | f'This is a "string"' +16 | f'\'This\' is a "string"' + | + = help: Remove backslash + +ℹ Safe fix +11 11 | +12 12 | # Same as above, but with f-strings +13 13 | f"This is a \'string\'" # Q004 +14 |-f"'This' is a \'string\'" # Q004 + 14 |+f"'This' is a 'string'" # Q004 +15 15 | f'This is a "string"' +16 16 | f'\'This\' is a "string"' +17 17 | fr"This is a \'string\'" + +singles_escaped_unnecessary.py:21:5: Q004 [*] Unnecessary escape on inner quote character + | +19 | this_should_raise_Q004 = ( +20 | f"This is a" +21 | f"\'string\'" # Q004 + | ^^^^^^^^^^^^^ Q004 +22 | ) + | + = help: Remove backslash + +ℹ Safe fix +18 18 | fR"This is a \'string\'" +19 19 | this_should_raise_Q004 = ( +20 20 | f"This is a" +21 |- f"\'string\'" # Q004 + 21 |+ f"'string'" # Q004 +22 22 | ) +23 23 | +24 24 | # Nested f-strings (Python 3.12+) + +singles_escaped_unnecessary.py:31:1: Q004 [*] Unnecessary escape on inner quote character + | +29 | # +30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 | f"\'foo\' {"foo"}" # Q004 + | ^^^^^^^^^^^^^^^^^^ Q004 +32 | f"\'foo\' {f"foo"}" # Q004 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +28 28 | # f'"foo" {"nested"}' +29 29 | # +30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 |-f"\'foo\' {"foo"}" # Q004 + 31 |+f"'foo' {"foo"}" # Q004 +32 32 | f"\'foo\' {f"foo"}" # Q004 +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 +34 34 | + +singles_escaped_unnecessary.py:32:1: Q004 [*] Unnecessary escape on inner quote character + | +30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 | f"\'foo\' {"foo"}" # Q004 +32 | f"\'foo\' {f"foo"}" # Q004 + | ^^^^^^^^^^^^^^^^^^^ Q004 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +29 29 | # +30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 31 | f"\'foo\' {"foo"}" # Q004 +32 |-f"\'foo\' {f"foo"}" # Q004 + 32 |+f"'foo' {f"foo"}" # Q004 +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 +34 34 | +35 35 | f"normal {f"nested"} normal" + +singles_escaped_unnecessary.py:33:1: Q004 [*] Unnecessary escape on inner quote character + | +31 | f"\'foo\' {"foo"}" # Q004 +32 | f"\'foo\' {f"foo"}" # Q004 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +34 | +35 | f"normal {f"nested"} normal" + | + = help: Remove backslash + +ℹ Safe fix +30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 31 | f"\'foo\' {"foo"}" # Q004 +32 32 | f"\'foo\' {f"foo"}" # Q004 +33 |-f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + 33 |+f"'foo' {f"\'foo\'"} ''" # Q004 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q004 + +singles_escaped_unnecessary.py:33:12: Q004 [*] Unnecessary escape on inner quote character + | +31 | f"\'foo\' {"foo"}" # Q004 +32 | f"\'foo\' {f"foo"}" # Q004 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + | ^^^^^^^^^^ Q004 +34 | +35 | f"normal {f"nested"} normal" + | + = help: Remove backslash + +ℹ Safe fix +30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 31 | f"\'foo\' {"foo"}" # Q004 +32 32 | f"\'foo\' {f"foo"}" # Q004 +33 |-f"\'foo\' {f"\'foo\'"} \'\'" # Q004 + 33 |+f"\'foo\' {f"'foo'"} \'\'" # Q004 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q004 + +singles_escaped_unnecessary.py:36:1: Q004 [*] Unnecessary escape on inner quote character + | +35 | f"normal {f"nested"} normal" +36 | f"\'normal\' {f"nested"} normal" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q004 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 |-f"\'normal\' {f"nested"} normal" # Q004 + 36 |+f"'normal' {f"nested"} normal" # Q004 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + +singles_escaped_unnecessary.py:37:1: Q004 [*] Unnecessary escape on inner quote character + | +35 | f"normal {f"nested"} normal" +36 | f"\'normal\' {f"nested"} normal" # Q004 +37 | f"\'normal\' {f"nested"} 'single quotes'" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q004 +37 |-f"\'normal\' {f"nested"} 'single quotes'" + 37 |+f"'normal' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 +40 40 | + +singles_escaped_unnecessary.py:38:1: Q004 [*] Unnecessary escape on inner quote character + | +36 | f"\'normal\' {f"nested"} normal" # Q004 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q004 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 |-f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 + 38 |+f"'normal' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 +40 40 | +41 41 | # Make sure we do not unescape quotes + +singles_escaped_unnecessary.py:38:15: Q004 [*] Unnecessary escape on inner quote character + | +36 | f"\'normal\' {f"nested"} normal" # Q004 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + | + = help: Remove backslash + +ℹ Safe fix +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q004 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 |-f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 + 38 |+f"\'normal\' {f"'nested' {"other"} normal"} 'single quotes'" # Q004 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 +40 40 | +41 41 | # Make sure we do not unescape quotes + +singles_escaped_unnecessary.py:39:1: Q004 [*] Unnecessary escape on inner quote character + | +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +40 | +41 | # Make sure we do not unescape quotes + | + = help: Remove backslash + +ℹ Safe fix +36 36 | f"\'normal\' {f"nested"} normal" # Q004 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 |-f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + 39 |+f"'normal' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 +40 40 | +41 41 | # Make sure we do not unescape quotes +42 42 | this_is_fine = "This is an \\'escaped\\' quote" + +singles_escaped_unnecessary.py:39:15: Q004 [*] Unnecessary escape on inner quote character + | +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +40 | +41 | # Make sure we do not unescape quotes + | + = help: Remove backslash + +ℹ Safe fix +36 36 | f"\'normal\' {f"nested"} normal" # Q004 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q004 +39 |-f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q004 + 39 |+f"\'normal\' {f"'nested' {"other"} 'single quotes'"} normal" # Q004 +40 40 | +41 41 | # Make sure we do not unescape quotes +42 42 | this_is_fine = "This is an \\'escaped\\' quote" + +singles_escaped_unnecessary.py:43:26: Q004 [*] Unnecessary escape on inner quote character + | +41 | # Make sure we do not unescape quotes +42 | this_is_fine = "This is an \\'escaped\\' quote" +43 | this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 + | + = help: Remove backslash + +ℹ Safe fix +40 40 | +41 41 | # Make sure we do not unescape quotes +42 42 | this_is_fine = "This is an \\'escaped\\' quote" +43 |-this_should_raise_Q004 = "This is an \\\'escaped\\\' quote with an extra backslash" + 43 |+this_should_raise_Q004 = "This is an \\'escaped\\' quote with an extra backslash" + + diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap new file mode 100644 index 0000000000..4e7c09e9cd --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_unnecessary.py.snap @@ -0,0 +1,382 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +doubles_escaped_unnecessary.py:1:26: Q004 [*] Unnecessary escape on inner quote character + | +1 | this_should_raise_Q004 = 'This is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^ Q004 +2 | this_should_raise_Q004 = 'This is \\ a \\\"string\"' +3 | this_is_fine = '"This" is a \"string\"' + | + = help: Remove backslash + +ℹ Safe fix +1 |-this_should_raise_Q004 = 'This is a \"string\"' + 1 |+this_should_raise_Q004 = 'This is a "string"' +2 2 | this_should_raise_Q004 = 'This is \\ a \\\"string\"' +3 3 | this_is_fine = '"This" is a \"string\"' +4 4 | this_is_fine = "This is a 'string'" + +doubles_escaped_unnecessary.py:2:26: Q004 [*] Unnecessary escape on inner quote character + | +1 | this_should_raise_Q004 = 'This is a \"string\"' +2 | this_should_raise_Q004 = 'This is \\ a \\\"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +3 | this_is_fine = '"This" is a \"string\"' +4 | this_is_fine = "This is a 'string'" + | + = help: Remove backslash + +ℹ Safe fix +1 1 | this_should_raise_Q004 = 'This is a \"string\"' +2 |-this_should_raise_Q004 = 'This is \\ a \\\"string\"' + 2 |+this_should_raise_Q004 = 'This is \\ a \\"string"' +3 3 | this_is_fine = '"This" is a \"string\"' +4 4 | this_is_fine = "This is a 'string'" +5 5 | this_is_fine = "\"This\" is a 'string'" + +doubles_escaped_unnecessary.py:3:16: Q004 [*] Unnecessary escape on inner quote character + | +1 | this_should_raise_Q004 = 'This is a \"string\"' +2 | this_should_raise_Q004 = 'This is \\ a \\\"string\"' +3 | this_is_fine = '"This" is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +4 | this_is_fine = "This is a 'string'" +5 | this_is_fine = "\"This\" is a 'string'" + | + = help: Remove backslash + +ℹ Safe fix +1 1 | this_should_raise_Q004 = 'This is a \"string\"' +2 2 | this_should_raise_Q004 = 'This is \\ a \\\"string\"' +3 |-this_is_fine = '"This" is a \"string\"' + 3 |+this_is_fine = '"This" is a "string"' +4 4 | this_is_fine = "This is a 'string'" +5 5 | this_is_fine = "\"This\" is a 'string'" +6 6 | this_is_fine = r'This is a \"string\"' + +doubles_escaped_unnecessary.py:10:5: Q004 [*] Unnecessary escape on inner quote character + | + 8 | this_should_raise_Q004 = ( + 9 | 'This is a' +10 | '\"string\"' + | ^^^^^^^^^^^^ Q004 +11 | ) + | + = help: Remove backslash + +ℹ Safe fix +7 7 | this_is_fine = R'This is a \"string\"' +8 8 | this_should_raise_Q004 = ( +9 9 | 'This is a' +10 |- '\"string\"' + 10 |+ '"string"' +11 11 | ) +12 12 | +13 13 | # Same as above, but with f-strings + +doubles_escaped_unnecessary.py:14:1: Q004 [*] Unnecessary escape on inner quote character + | +13 | # Same as above, but with f-strings +14 | f'This is a \"string\"' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q004 +15 | f'This is \\ a \\\"string\"' # Q004 +16 | f'"This" is a \"string\"' + | + = help: Remove backslash + +ℹ Safe fix +11 11 | ) +12 12 | +13 13 | # Same as above, but with f-strings +14 |-f'This is a \"string\"' # Q004 + 14 |+f'This is a "string"' # Q004 +15 15 | f'This is \\ a \\\"string\"' # Q004 +16 16 | f'"This" is a \"string\"' +17 17 | f"This is a 'string'" + +doubles_escaped_unnecessary.py:15:1: Q004 [*] Unnecessary escape on inner quote character + | +13 | # Same as above, but with f-strings +14 | f'This is a \"string\"' # Q004 +15 | f'This is \\ a \\\"string\"' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +16 | f'"This" is a \"string\"' +17 | f"This is a 'string'" + | + = help: Remove backslash + +ℹ Safe fix +12 12 | +13 13 | # Same as above, but with f-strings +14 14 | f'This is a \"string\"' # Q004 +15 |-f'This is \\ a \\\"string\"' # Q004 + 15 |+f'This is \\ a \\"string"' # Q004 +16 16 | f'"This" is a \"string\"' +17 17 | f"This is a 'string'" +18 18 | f"\"This\" is a 'string'" + +doubles_escaped_unnecessary.py:16:1: Q004 [*] Unnecessary escape on inner quote character + | +14 | f'This is a \"string\"' # Q004 +15 | f'This is \\ a \\\"string\"' # Q004 +16 | f'"This" is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +17 | f"This is a 'string'" +18 | f"\"This\" is a 'string'" + | + = help: Remove backslash + +ℹ Safe fix +13 13 | # Same as above, but with f-strings +14 14 | f'This is a \"string\"' # Q004 +15 15 | f'This is \\ a \\\"string\"' # Q004 +16 |-f'"This" is a \"string\"' + 16 |+f'"This" is a "string"' +17 17 | f"This is a 'string'" +18 18 | f"\"This\" is a 'string'" +19 19 | fr'This is a \"string\"' + +doubles_escaped_unnecessary.py:23:5: Q004 [*] Unnecessary escape on inner quote character + | +21 | this_should_raise_Q004 = ( +22 | f'This is a' +23 | f'\"string\"' # Q004 + | ^^^^^^^^^^^^^ Q004 +24 | ) + | + = help: Remove backslash + +ℹ Safe fix +20 20 | fR'This is a \"string\"' +21 21 | this_should_raise_Q004 = ( +22 22 | f'This is a' +23 |- f'\"string\"' # Q004 + 23 |+ f'"string"' # Q004 +24 24 | ) +25 25 | +26 26 | # Nested f-strings (Python 3.12+) + +doubles_escaped_unnecessary.py:33:1: Q004 [*] Unnecessary escape on inner quote character + | +31 | # +32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 | f'\"foo\" {'nested'}' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^ Q004 +34 | f'\"foo\" {f'nested'}' # Q004 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +30 30 | # f"'foo' {'nested'}" +31 31 | # +32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 |-f'\"foo\" {'nested'}' # Q004 + 33 |+f'"foo" {'nested'}' # Q004 +34 34 | f'\"foo\" {f'nested'}' # Q004 +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 +36 36 | + +doubles_escaped_unnecessary.py:34:1: Q004 [*] Unnecessary escape on inner quote character + | +32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 | f'\"foo\" {'nested'}' # Q004 +34 | f'\"foo\" {f'nested'}' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^ Q004 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +31 31 | # +32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 33 | f'\"foo\" {'nested'}' # Q004 +34 |-f'\"foo\" {f'nested'}' # Q004 + 34 |+f'"foo" {f'nested'}' # Q004 +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 +36 36 | +37 37 | f'normal {f'nested'} normal' + +doubles_escaped_unnecessary.py:35:1: Q004 [*] Unnecessary escape on inner quote character + | +33 | f'\"foo\" {'nested'}' # Q004 +34 | f'\"foo\" {f'nested'}' # Q004 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +36 | +37 | f'normal {f'nested'} normal' + | + = help: Remove backslash + +ℹ Safe fix +32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 33 | f'\"foo\" {'nested'}' # Q004 +34 34 | f'\"foo\" {f'nested'}' # Q004 +35 |-f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + 35 |+f'"foo" {f'\"nested\"'} ""' # Q004 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q004 + +doubles_escaped_unnecessary.py:35:12: Q004 [*] Unnecessary escape on inner quote character + | +33 | f'\"foo\" {'nested'}' # Q004 +34 | f'\"foo\" {f'nested'}' # Q004 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + | ^^^^^^^^^^^^^ Q004 +36 | +37 | f'normal {f'nested'} normal' + | + = help: Remove backslash + +ℹ Safe fix +32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 33 | f'\"foo\" {'nested'}' # Q004 +34 34 | f'\"foo\" {f'nested'}' # Q004 +35 |-f'\"foo\" {f'\"nested\"'} \"\"' # Q004 + 35 |+f'\"foo\" {f'"nested"'} \"\"' # Q004 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q004 + +doubles_escaped_unnecessary.py:38:1: Q004 [*] Unnecessary escape on inner quote character + | +37 | f'normal {f'nested'} normal' +38 | f'\"normal\" {f'nested'} normal' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q004 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 |-f'\"normal\" {f'nested'} normal' # Q004 + 38 |+f'"normal" {f'nested'} normal' # Q004 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + +doubles_escaped_unnecessary.py:39:1: Q004 [*] Unnecessary escape on inner quote character + | +37 | f'normal {f'nested'} normal' +38 | f'\"normal\" {f'nested'} normal' # Q004 +39 | f'\"normal\" {f'nested'} "double quotes"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q004 +39 |-f'\"normal\" {f'nested'} "double quotes"' + 39 |+f'"normal" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 +42 42 | + +doubles_escaped_unnecessary.py:40:1: Q004 [*] Unnecessary escape on inner quote character + | +38 | f'\"normal\" {f'nested'} normal' # Q004 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q004 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 |-f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 + 40 |+f'"normal" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 +42 42 | +43 43 | # Make sure we do not unescape quotes + +doubles_escaped_unnecessary.py:40:15: Q004 [*] Unnecessary escape on inner quote character + | +38 | f'\"normal\" {f'nested'} normal' # Q004 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + | + = help: Remove backslash + +ℹ Safe fix +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q004 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 |-f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 + 40 |+f'\"normal\" {f'"nested" {'other'} normal'} "double quotes"' # Q004 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 +42 42 | +43 43 | # Make sure we do not unescape quotes + +doubles_escaped_unnecessary.py:41:1: Q004 [*] Unnecessary escape on inner quote character + | +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +42 | +43 | # Make sure we do not unescape quotes + | + = help: Remove backslash + +ℹ Safe fix +38 38 | f'\"normal\" {f'nested'} normal' # Q004 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 |-f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + 41 |+f'"normal" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 +42 42 | +43 43 | # Make sure we do not unescape quotes +44 44 | this_is_fine = 'This is an \\"escaped\\" quote' + +doubles_escaped_unnecessary.py:41:15: Q004 [*] Unnecessary escape on inner quote character + | +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 +42 | +43 | # Make sure we do not unescape quotes + | + = help: Remove backslash + +ℹ Safe fix +38 38 | f'\"normal\" {f'nested'} normal' # Q004 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q004 +41 |-f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q004 + 41 |+f'\"normal\" {f'"nested" {'other'} "double quotes"'} normal' # Q004 +42 42 | +43 43 | # Make sure we do not unescape quotes +44 44 | this_is_fine = 'This is an \\"escaped\\" quote' + +doubles_escaped_unnecessary.py:45:26: Q004 [*] Unnecessary escape on inner quote character + | +43 | # Make sure we do not unescape quotes +44 | this_is_fine = 'This is an \\"escaped\\" quote' +45 | this_should_raise_Q004 = 'This is an \\\"escaped\\\" quote with an extra backslash' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q004 + | + = help: Remove backslash + +ℹ Safe fix +42 42 | +43 43 | # Make sure we do not unescape quotes +44 44 | this_is_fine = 'This is an \\"escaped\\" quote' +45 |-this_should_raise_Q004 = 'This is an \\\"escaped\\\" quote with an extra backslash' + 45 |+this_should_raise_Q004 = 'This is an \\"escaped\\" quote with an extra backslash' + + diff --git a/ruff.schema.json b/ruff.schema.json index 989b21078f..4588721abb 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3277,6 +3277,7 @@ "Q001", "Q002", "Q003", + "Q004", "RET", "RET5", "RET50", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 78334cc3b5..d4bf715dd9 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -76,6 +76,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "unexpected-spaces-around-keyword-parameter-equals", "unicode-kind-prefix", "unnecessary-class-parentheses", + "unnecessary-escaped-quote", "useless-semicolon", "whitespace-after-open-bracket", "whitespace-before-close-bracket",