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 0000000000000..a47d81b10c78f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped_unnecessary.py @@ -0,0 +1,45 @@ +this_should_raise_Q100 = 'This is a \"string\"' +this_should_raise_Q100 = '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_Q100 = ( + 'This is a' + '\"string\"' +) + +# Same as above, but with f-strings +f'This is a \"string\"' # Q100 +f'This is \\ a \\\"string\"' # Q100 +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_Q100 = ( + f'This is a' + f'\"string\"' # Q100 +) + +# 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'}' # Q100 +f'\"foo\" {f'nested'}' # Q100 +f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + +f'normal {f'nested'} normal' +f'\"normal\" {f'nested'} normal' # Q100 +f'\"normal\" {f'nested'} "double quotes"' +f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + +# Make sure we do not unescape quotes +this_is_fine = 'This is an \\"escaped\\" quote' +this_should_raise_Q100 = '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 0000000000000..47804f0d638dd --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped_unnecessary.py @@ -0,0 +1,43 @@ +this_should_raise_Q100 = "This is a \'string\'" +this_should_raise_Q100 = "'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_Q100 = ( + "This is a" + "\'string\'" +) + +# Same as above, but with f-strings +f"This is a \'string\'" # Q100 +f"'This' is a \'string\'" # Q100 +f'This is a "string"' +f'\'This\' is a "string"' +fr"This is a \'string\'" +fR"This is a \'string\'" +this_should_raise_Q100 = ( + f"This is a" + f"\'string\'" # Q100 +) + +# 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"}" # Q100 +f"\'foo\' {f"foo"}" # Q100 +f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + +f"normal {f"nested"} normal" +f"\'normal\' {f"nested"} normal" # Q100 +f"\'normal\' {f"nested"} 'single quotes'" +f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + +# Make sure we do not unescape quotes +this_is_fine = "This is an \\'escaped\\' quote" +this_should_raise_Q100 = "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 900f75eb27601..9f3f0866f7e63 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 8efb7b5e2c460..3daef72032a99 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, "100") => (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 1d178d1f1412d..07ede87903f59 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 ab4f2ef42ebbe..506269abc8c8c 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 @@ -1,3 +1,4 @@ +use super::super::settings::Quote; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{is_triple_quote, leading_quote}; @@ -48,6 +49,43 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote { } } +/// ## What it does +/// Checks for strings that include escaped quotes not matching the outer +/// quotes, and suggests removing the unnecessary backslash. +/// +/// ## Why is this bad? +/// It's escaping something that does not need escaping. +/// +/// ## 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!("Do not escape quotes that do not need escaping") + } + + fn fix_title(&self) -> String { + "Remove backslash from quotes that do not need escaping".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, } } @@ -146,7 +191,10 @@ pub(crate) fn avoidable_escaped_quote( "{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, @@ -162,7 +210,11 @@ pub(crate) fn avoidable_escaped_quote( 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)); + fstrings.push(FStringContext::new( + check_for_escaped_quote, + tok_range, + quotes_settings.inline_quotes, + )); } Tok::FStringMiddle { value: string_contents, @@ -207,7 +259,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 +289,149 @@ pub(crate) fn avoidable_escaped_quote( } } -fn unescape_string(value: &str) -> String { +/// Q100 +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(); + + 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(leading.opposite().as_char(), string_contents) { + 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 text.contains(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(context.quote_style.opposite().as_char(), string_contents) + { + context.push_fstring_middle_range(tok_range); + } + } + Tok::FStringEnd => { + let Some(context) = fstrings.pop() else { + continue; + }; + if context.middle_ranges_with_escapes.is_empty() { + // There are no `FStringMiddle` tokens containing any escaped + // quotes. + continue; + } + let mut diagnostic = Diagnostic::new( + UnnecessaryEscapedQuote, + TextRange::new(context.start_range.start(), tok_range.end()), + ); + let mut fstring_middle_edits = + context.middle_ranges_with_escapes.iter().map(|&range| { + Edit::range_replacement( + unescape_string( + locator.slice(range), + context.quote_style.opposite().as_char(), + ), + range, + ) + }); + diagnostic.set_fix(Fix::safe_edits( + fstring_middle_edits.next().unwrap(), + fstring_middle_edits, + )); + diagnostics.push(diagnostic); + } + _ => {} + } + } +} + +fn contains_escaped_quote(quote: char, haystack: &str) -> bool { + let mut chars = haystack.chars().peekable(); + let mut backslashes = 0; + while let Some(char) = chars.next() { + if char != '\\' { + backslashes = 0; + continue; + } + // If we're at the end of the line + let Some(next_char) = chars.peek() else { + continue; + }; + // Remove quote escape + if *next_char == quote && backslashes % 2 == 0 { + return true; + } + backslashes += 1; + } + + false +} + +fn unescape_string(value: &str, remove_quote: char) -> String { let mut fixed_contents = String::with_capacity(value.len()); let mut chars = value.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 +440,11 @@ fn unescape_string(value: &str) -> String { continue; }; // Remove quote escape - if matches!(*next_char, '\'' | '"') { + if *next_char == remove_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 0000000000000..edf67343f9ccb --- /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: Q100 [*] Do not escape quotes that do not need escaping + | +1 | this_should_raise_Q100 = "This is a \'string\'" + | ^^^^^^^^^^^^^^^^^^^^^^ Q100 +2 | this_should_raise_Q100 = "'This' is a \'string\'" +3 | this_is_fine = 'This is a "string"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +1 |-this_should_raise_Q100 = "This is a \'string\'" + 1 |+this_should_raise_Q100 = "This is a 'string'" +2 2 | this_should_raise_Q100 = "'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: Q100 [*] Do not escape quotes that do not need escaping + | +1 | this_should_raise_Q100 = "This is a \'string\'" +2 | this_should_raise_Q100 = "'This' is a \'string\'" + | ^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +3 | this_is_fine = 'This is a "string"' +4 | this_is_fine = '\'This\' is a "string"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +1 1 | this_should_raise_Q100 = "This is a \'string\'" +2 |-this_should_raise_Q100 = "'This' is a \'string\'" + 2 |+this_should_raise_Q100 = "'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: Q100 [*] Do not escape quotes that do not need escaping + | + 7 | this_should_raise_Q100 = ( + 8 | "This is a" + 9 | "\'string\'" + | ^^^^^^^^^^^^ Q100 +10 | ) + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +6 6 | this_is_fine = R"This is a \'string\'" +7 7 | this_should_raise_Q100 = ( +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: Q100 [*] Do not escape quotes that do not need escaping + | +12 | # Same as above, but with f-strings +13 | f"This is a \'string\'" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q100 +14 | f"'This' is a \'string\'" # Q100 +15 | f'This is a "string"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +10 10 | ) +11 11 | +12 12 | # Same as above, but with f-strings +13 |-f"This is a \'string\'" # Q100 + 13 |+f"This is a 'string'" # Q100 +14 14 | f"'This' is a \'string\'" # Q100 +15 15 | f'This is a "string"' +16 16 | f'\'This\' is a "string"' + +singles_escaped_unnecessary.py:14:1: Q100 [*] Do not escape quotes that do not need escaping + | +12 | # Same as above, but with f-strings +13 | f"This is a \'string\'" # Q100 +14 | f"'This' is a \'string\'" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +15 | f'This is a "string"' +16 | f'\'This\' is a "string"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +11 11 | +12 12 | # Same as above, but with f-strings +13 13 | f"This is a \'string\'" # Q100 +14 |-f"'This' is a \'string\'" # Q100 + 14 |+f"'This' is a 'string'" # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +19 | this_should_raise_Q100 = ( +20 | f"This is a" +21 | f"\'string\'" # Q100 + | ^^^^^^^^^^^^^ Q100 +22 | ) + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +18 18 | fR"This is a \'string\'" +19 19 | this_should_raise_Q100 = ( +20 20 | f"This is a" +21 |- f"\'string\'" # Q100 + 21 |+ f"'string'" # Q100 +22 22 | ) +23 23 | +24 24 | # Nested f-strings (Python 3.12+) + +singles_escaped_unnecessary.py:31:1: Q100 [*] Do not escape quotes that do not need escaping + | +29 | # +30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 | f"\'foo\' {"foo"}" # Q100 + | ^^^^^^^^^^^^^^^^^^ Q100 +32 | f"\'foo\' {f"foo"}" # Q100 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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"}" # Q100 + 31 |+f"'foo' {"foo"}" # Q100 +32 32 | f"\'foo\' {f"foo"}" # Q100 +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 +34 34 | + +singles_escaped_unnecessary.py:32:1: Q100 [*] Do not escape quotes that do not need escaping + | +30 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +31 | f"\'foo\' {"foo"}" # Q100 +32 | f"\'foo\' {f"foo"}" # Q100 + | ^^^^^^^^^^^^^^^^^^^ Q100 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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"}" # Q100 +32 |-f"\'foo\' {f"foo"}" # Q100 + 32 |+f"'foo' {f"foo"}" # Q100 +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 +34 34 | +35 35 | f"normal {f"nested"} normal" + +singles_escaped_unnecessary.py:33:1: Q100 [*] Do not escape quotes that do not need escaping + | +31 | f"\'foo\' {"foo"}" # Q100 +32 | f"\'foo\' {f"foo"}" # Q100 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +34 | +35 | f"normal {f"nested"} normal" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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"}" # Q100 +32 32 | f"\'foo\' {f"foo"}" # Q100 +33 |-f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + 33 |+f"'foo' {f"\'foo\'"} ''" # Q100 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q100 + +singles_escaped_unnecessary.py:33:12: Q100 [*] Do not escape quotes that do not need escaping + | +31 | f"\'foo\' {"foo"}" # Q100 +32 | f"\'foo\' {f"foo"}" # Q100 +33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + | ^^^^^^^^^^ Q100 +34 | +35 | f"normal {f"nested"} normal" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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"}" # Q100 +32 32 | f"\'foo\' {f"foo"}" # Q100 +33 |-f"\'foo\' {f"\'foo\'"} \'\'" # Q100 + 33 |+f"\'foo\' {f"'foo'"} \'\'" # Q100 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q100 + +singles_escaped_unnecessary.py:36:1: Q100 [*] Do not escape quotes that do not need escaping + | +35 | f"normal {f"nested"} normal" +36 | f"\'normal\' {f"nested"} normal" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +33 33 | f"\'foo\' {f"\'foo\'"} \'\'" # Q100 +34 34 | +35 35 | f"normal {f"nested"} normal" +36 |-f"\'normal\' {f"nested"} normal" # Q100 + 36 |+f"'normal' {f"nested"} normal" # Q100 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + +singles_escaped_unnecessary.py:37:1: Q100 [*] Do not escape quotes that do not need escaping + | +35 | f"normal {f"nested"} normal" +36 | f"\'normal\' {f"nested"} normal" # Q100 +37 | f"\'normal\' {f"nested"} 'single quotes'" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +34 34 | +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q100 +37 |-f"\'normal\' {f"nested"} 'single quotes'" + 37 |+f"'normal' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 +40 40 | + +singles_escaped_unnecessary.py:38:1: Q100 [*] Do not escape quotes that do not need escaping + | +36 | f"\'normal\' {f"nested"} normal" # Q100 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q100 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 |-f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 + 38 |+f"'normal' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 +40 40 | +41 41 | # Make sure we do not unescape quotes + +singles_escaped_unnecessary.py:38:15: Q100 [*] Do not escape quotes that do not need escaping + | +36 | f"\'normal\' {f"nested"} normal" # Q100 +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +35 35 | f"normal {f"nested"} normal" +36 36 | f"\'normal\' {f"nested"} normal" # Q100 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 |-f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 + 38 |+f"\'normal\' {f"'nested' {"other"} normal"} 'single quotes'" # Q100 +39 39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 +40 40 | +41 41 | # Make sure we do not unescape quotes + +singles_escaped_unnecessary.py:39:1: Q100 [*] Do not escape quotes that do not need escaping + | +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +40 | +41 | # Make sure we do not unescape quotes + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +36 36 | f"\'normal\' {f"nested"} normal" # Q100 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 |-f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + 39 |+f"'normal' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +37 | f"\'normal\' {f"nested"} 'single quotes'" +38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 | f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +40 | +41 | # Make sure we do not unescape quotes + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +36 36 | f"\'normal\' {f"nested"} normal" # Q100 +37 37 | f"\'normal\' {f"nested"} 'single quotes'" +38 38 | f"\'normal\' {f"\'nested\' {"other"} normal"} 'single quotes'" # Q100 +39 |-f"\'normal\' {f"\'nested\' {"other"} 'single quotes'"} normal" # Q100 + 39 |+f"\'normal\' {f"'nested' {"other"} 'single quotes'"} normal" # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +41 | # Make sure we do not unescape quotes +42 | this_is_fine = "This is an \\'escaped\\' quote" +43 | this_should_raise_Q100 = "This is an \\\'escaped\\\' quote with an extra backslash" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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_Q100 = "This is an \\\'escaped\\\' quote with an extra backslash" + 43 |+this_should_raise_Q100 = "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 0000000000000..fe63d4aab2483 --- /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: Q100 [*] Do not escape quotes that do not need escaping + | +1 | this_should_raise_Q100 = 'This is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^ Q100 +2 | this_should_raise_Q100 = 'This is \\ a \\\"string\"' +3 | this_is_fine = '"This" is a \"string\"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +1 |-this_should_raise_Q100 = 'This is a \"string\"' + 1 |+this_should_raise_Q100 = 'This is a "string"' +2 2 | this_should_raise_Q100 = '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: Q100 [*] Do not escape quotes that do not need escaping + | +1 | this_should_raise_Q100 = 'This is a \"string\"' +2 | this_should_raise_Q100 = 'This is \\ a \\\"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +3 | this_is_fine = '"This" is a \"string\"' +4 | this_is_fine = "This is a 'string'" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +1 1 | this_should_raise_Q100 = 'This is a \"string\"' +2 |-this_should_raise_Q100 = 'This is \\ a \\\"string\"' + 2 |+this_should_raise_Q100 = '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: Q100 [*] Do not escape quotes that do not need escaping + | +1 | this_should_raise_Q100 = 'This is a \"string\"' +2 | this_should_raise_Q100 = 'This is \\ a \\\"string\"' +3 | this_is_fine = '"This" is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +4 | this_is_fine = "This is a 'string'" +5 | this_is_fine = "\"This\" is a 'string'" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +1 1 | this_should_raise_Q100 = 'This is a \"string\"' +2 2 | this_should_raise_Q100 = '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: Q100 [*] Do not escape quotes that do not need escaping + | + 8 | this_should_raise_Q100 = ( + 9 | 'This is a' +10 | '\"string\"' + | ^^^^^^^^^^^^ Q100 +11 | ) + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +7 7 | this_is_fine = R'This is a \"string\"' +8 8 | this_should_raise_Q100 = ( +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: Q100 [*] Do not escape quotes that do not need escaping + | +13 | # Same as above, but with f-strings +14 | f'This is a \"string\"' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q100 +15 | f'This is \\ a \\\"string\"' # Q100 +16 | f'"This" is a \"string\"' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +11 11 | ) +12 12 | +13 13 | # Same as above, but with f-strings +14 |-f'This is a \"string\"' # Q100 + 14 |+f'This is a "string"' # Q100 +15 15 | f'This is \\ a \\\"string\"' # Q100 +16 16 | f'"This" is a \"string\"' +17 17 | f"This is a 'string'" + +doubles_escaped_unnecessary.py:15:1: Q100 [*] Do not escape quotes that do not need escaping + | +13 | # Same as above, but with f-strings +14 | f'This is a \"string\"' # Q100 +15 | f'This is \\ a \\\"string\"' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +16 | f'"This" is a \"string\"' +17 | f"This is a 'string'" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +12 12 | +13 13 | # Same as above, but with f-strings +14 14 | f'This is a \"string\"' # Q100 +15 |-f'This is \\ a \\\"string\"' # Q100 + 15 |+f'This is \\ a \\"string"' # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +14 | f'This is a \"string\"' # Q100 +15 | f'This is \\ a \\\"string\"' # Q100 +16 | f'"This" is a \"string\"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +17 | f"This is a 'string'" +18 | f"\"This\" is a 'string'" + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +13 13 | # Same as above, but with f-strings +14 14 | f'This is a \"string\"' # Q100 +15 15 | f'This is \\ a \\\"string\"' # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +21 | this_should_raise_Q100 = ( +22 | f'This is a' +23 | f'\"string\"' # Q100 + | ^^^^^^^^^^^^^ Q100 +24 | ) + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +20 20 | fR'This is a \"string\"' +21 21 | this_should_raise_Q100 = ( +22 22 | f'This is a' +23 |- f'\"string\"' # Q100 + 23 |+ f'"string"' # Q100 +24 24 | ) +25 25 | +26 26 | # Nested f-strings (Python 3.12+) + +doubles_escaped_unnecessary.py:33:1: Q100 [*] Do not escape quotes that do not need escaping + | +31 | # +32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 | f'\"foo\" {'nested'}' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^ Q100 +34 | f'\"foo\" {f'nested'}' # Q100 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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'}' # Q100 + 33 |+f'"foo" {'nested'}' # Q100 +34 34 | f'\"foo\" {f'nested'}' # Q100 +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 +36 36 | + +doubles_escaped_unnecessary.py:34:1: Q100 [*] Do not escape quotes that do not need escaping + | +32 | # but as the actual string itself is invalid pre 3.12, we don't catch it. +33 | f'\"foo\" {'nested'}' # Q100 +34 | f'\"foo\" {f'nested'}' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^ Q100 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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'}' # Q100 +34 |-f'\"foo\" {f'nested'}' # Q100 + 34 |+f'"foo" {f'nested'}' # Q100 +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 +36 36 | +37 37 | f'normal {f'nested'} normal' + +doubles_escaped_unnecessary.py:35:1: Q100 [*] Do not escape quotes that do not need escaping + | +33 | f'\"foo\" {'nested'}' # Q100 +34 | f'\"foo\" {f'nested'}' # Q100 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +36 | +37 | f'normal {f'nested'} normal' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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'}' # Q100 +34 34 | f'\"foo\" {f'nested'}' # Q100 +35 |-f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + 35 |+f'"foo" {f'\"nested\"'} ""' # Q100 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q100 + +doubles_escaped_unnecessary.py:35:12: Q100 [*] Do not escape quotes that do not need escaping + | +33 | f'\"foo\" {'nested'}' # Q100 +34 | f'\"foo\" {f'nested'}' # Q100 +35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + | ^^^^^^^^^^^^^ Q100 +36 | +37 | f'normal {f'nested'} normal' + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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'}' # Q100 +34 34 | f'\"foo\" {f'nested'}' # Q100 +35 |-f'\"foo\" {f'\"nested\"'} \"\"' # Q100 + 35 |+f'\"foo\" {f'"nested"'} \"\"' # Q100 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q100 + +doubles_escaped_unnecessary.py:38:1: Q100 [*] Do not escape quotes that do not need escaping + | +37 | f'normal {f'nested'} normal' +38 | f'\"normal\" {f'nested'} normal' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +35 35 | f'\"foo\" {f'\"nested\"'} \"\"' # Q100 +36 36 | +37 37 | f'normal {f'nested'} normal' +38 |-f'\"normal\" {f'nested'} normal' # Q100 + 38 |+f'"normal" {f'nested'} normal' # Q100 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + +doubles_escaped_unnecessary.py:39:1: Q100 [*] Do not escape quotes that do not need escaping + | +37 | f'normal {f'nested'} normal' +38 | f'\"normal\" {f'nested'} normal' # Q100 +39 | f'\"normal\" {f'nested'} "double quotes"' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +36 36 | +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q100 +39 |-f'\"normal\" {f'nested'} "double quotes"' + 39 |+f'"normal" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 +42 42 | + +doubles_escaped_unnecessary.py:40:1: Q100 [*] Do not escape quotes that do not need escaping + | +38 | f'\"normal\" {f'nested'} normal' # Q100 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q100 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 |-f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 + 40 |+f'"normal" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 +42 42 | +43 43 | # Make sure we do not unescape quotes + +doubles_escaped_unnecessary.py:40:15: Q100 [*] Do not escape quotes that do not need escaping + | +38 | f'\"normal\" {f'nested'} normal' # Q100 +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +37 37 | f'normal {f'nested'} normal' +38 38 | f'\"normal\" {f'nested'} normal' # Q100 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 |-f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 + 40 |+f'\"normal\" {f'"nested" {'other'} normal'} "double quotes"' # Q100 +41 41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 +42 42 | +43 43 | # Make sure we do not unescape quotes + +doubles_escaped_unnecessary.py:41:1: Q100 [*] Do not escape quotes that do not need escaping + | +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +42 | +43 | # Make sure we do not unescape quotes + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +38 38 | f'\"normal\" {f'nested'} normal' # Q100 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 |-f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + 41 |+f'"normal" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +39 | f'\"normal\" {f'nested'} "double quotes"' +40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 | f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 +42 | +43 | # Make sure we do not unescape quotes + | + = help: Remove backslash from quotes that do not need escaping + +ℹ Safe fix +38 38 | f'\"normal\" {f'nested'} normal' # Q100 +39 39 | f'\"normal\" {f'nested'} "double quotes"' +40 40 | f'\"normal\" {f'\"nested\" {'other'} normal'} "double quotes"' # Q100 +41 |-f'\"normal\" {f'\"nested\" {'other'} "double quotes"'} normal' # Q100 + 41 |+f'\"normal\" {f'"nested" {'other'} "double quotes"'} normal' # Q100 +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: Q100 [*] Do not escape quotes that do not need escaping + | +43 | # Make sure we do not unescape quotes +44 | this_is_fine = 'This is an \\"escaped\\" quote' +45 | this_should_raise_Q100 = 'This is an \\\"escaped\\\" quote with an extra backslash' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q100 + | + = help: Remove backslash from quotes that do not need escaping + +ℹ 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_Q100 = 'This is an \\\"escaped\\\" quote with an extra backslash' + 45 |+this_should_raise_Q100 = 'This is an \\"escaped\\" quote with an extra backslash' + + diff --git a/ruff.schema.json b/ruff.schema.json index 989b21078fcab..b813d5edd6a3b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3277,6 +3277,9 @@ "Q001", "Q002", "Q003", + "Q1", + "Q10", + "Q100", "RET", "RET5", "RET50", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 78334cc3b5f9a..d4bf715dd9b1e 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -76,6 +76,7 @@ "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",