From 17ea5b7249bc5d3730049e99911538d7da7f8aad Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 2 Mar 2024 17:58:02 +0100 Subject: [PATCH 01/13] Add tests --- ...docstring_doubles_mixed_quotes_module_singleline_var_1.py | 5 +++++ ...docstring_doubles_mixed_quotes_module_singleline_var_2.py | 5 +++++ ...docstring_singles_mixed_quotes_module_singleline_var_1.py | 5 +++++ ...docstring_singles_mixed_quotes_module_singleline_var_2.py | 5 +++++ crates/ruff_linter/src/rules/flake8_quotes/mod.rs | 4 ++++ 5 files changed, 24 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_1.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_2.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_1.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_2.py diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_1.py new file mode 100644 index 0000000000000..d454a607f9ee0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_1.py @@ -0,0 +1,5 @@ +""'Start with empty string' ' and lint docstring safely' + +def foo(): + pass +""" this is not a docstring """ diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_2.py new file mode 100644 index 0000000000000..ae372481a5d01 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_module_singleline_var_2.py @@ -0,0 +1,5 @@ +"Do not"' start with empty string' ' and lint docstring safely' + +def foo(): + pass +""" this is not a docstring """ diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_1.py new file mode 100644 index 0000000000000..255cd25167907 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_1.py @@ -0,0 +1,5 @@ +''"Start with empty string" ' and lint docstring safely' + +def foo(): + pass +""" this is not a docstring """ diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_2.py new file mode 100644 index 0000000000000..aadd151409758 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_module_singleline_var_2.py @@ -0,0 +1,5 @@ +'Do not'" start with empty string" ' and lint docstring safely' + +def foo(): + pass +""" this is not a docstring """ diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 07ede87903f59..3de8543cac9a3 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -127,6 +127,8 @@ mod tests { #[test_case(Path::new("docstring_singles_module_singleline.py"))] #[test_case(Path::new("docstring_singles_class.py"))] #[test_case(Path::new("docstring_singles_function.py"))] + #[test_case(Path::new("docstring_singles_mixed_quotes_module_singleline_var_1.py"))] + #[test_case(Path::new("docstring_singles_mixed_quotes_module_singleline_var_2.py"))] fn require_docstring_doubles(path: &Path) -> Result<()> { let snapshot = format!("require_docstring_doubles_over_{}", path.to_string_lossy()); let diagnostics = test_path( @@ -161,6 +163,8 @@ mod tests { #[test_case(Path::new("docstring_singles_module_singleline.py"))] #[test_case(Path::new("docstring_singles_class.py"))] #[test_case(Path::new("docstring_singles_function.py"))] + #[test_case(Path::new("docstring_doubles_mixed_quotes_module_singleline_var_1.py"))] + #[test_case(Path::new("docstring_doubles_mixed_quotes_module_singleline_var_2.py"))] fn require_docstring_singles(path: &Path) -> Result<()> { let snapshot = format!("require_docstring_singles_over_{}", path.to_string_lossy()); let diagnostics = test_path( From e65d99f381d7524d3bfd8b2bce3b6d156199071a Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 2 Mar 2024 18:00:09 +0100 Subject: [PATCH 02/13] Deactivate Q000 Not needed. Deactivation leads to more granular tests. --- crates/ruff_linter/src/rules/flake8_quotes/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 3de8543cac9a3..1b2ff3770c158 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -141,7 +141,6 @@ mod tests { avoid_escape: true, }, ..LinterSettings::for_rules(vec![ - Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, @@ -177,7 +176,6 @@ mod tests { avoid_escape: true, }, ..LinterSettings::for_rules(vec![ - Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, From b5c02e5c687d5b007e18325df58ab904baf6c7e2 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 2 Mar 2024 18:24:51 +0100 Subject: [PATCH 03/13] Handle text endings which end at quote --- .../rules/check_string_quotes.rs | 63 ++++++++++++++++--- ...xed_quotes_module_singleline_var_1.py.snap | 28 +++++++++ ...xed_quotes_module_singleline_var_2.py.snap | 35 +++++++++++ ...xed_quotes_module_singleline_var_1.py.snap | 12 ++++ ...xed_quotes_module_singleline_var_2.py.snap | 19 ++++++ 5 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index b182b36e899e1..d15221ea04239 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -2,7 +2,7 @@ use ruff_python_parser::lexer::LexResult; use ruff_python_parser::Tok; use ruff_text_size::{TextRange, TextSize}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_source_file::Locator; @@ -44,7 +44,9 @@ pub struct BadQuotesInlineString { preferred_quote: Quote, } -impl AlwaysFixableViolation for BadQuotesInlineString { +impl Violation for BadQuotesInlineString { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let BadQuotesInlineString { preferred_quote } = self; @@ -54,11 +56,11 @@ impl AlwaysFixableViolation for BadQuotesInlineString { } } - fn fix_title(&self) -> String { + fn fix_title(&self) -> Option { let BadQuotesInlineString { preferred_quote } = self; match preferred_quote { - Quote::Double => "Replace single quotes with double quotes".to_string(), - Quote::Single => "Replace double quotes with single quotes".to_string(), + Quote::Double => Some("Replace single quotes with double quotes".to_string()), + Quote::Single => Some("Replace double quotes with single quotes".to_string()), } } } @@ -155,7 +157,9 @@ pub struct BadQuotesDocstring { preferred_quote: Quote, } -impl AlwaysFixableViolation for BadQuotesDocstring { +impl Violation for BadQuotesDocstring { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let BadQuotesDocstring { preferred_quote } = self; @@ -165,11 +169,11 @@ impl AlwaysFixableViolation for BadQuotesDocstring { } } - fn fix_title(&self) -> String { + fn fix_title(&self) -> Option { let BadQuotesDocstring { preferred_quote } = self; match preferred_quote { - Quote::Double => "Replace single quotes docstring with double quotes".to_string(), - Quote::Single => "Replace double quotes docstring with single quotes".to_string(), + Quote::Double => Some("Replace single quotes docstring with double quotes".to_string()), + Quote::Single => Some("Replace double quotes docstring with single quotes".to_string()), } } } @@ -203,6 +207,12 @@ struct Trivia<'a> { is_multiline: bool, } +impl Trivia<'_> { + fn has_empty_text(&self) -> bool { + self.raw_text == "\"\"" || self.raw_text == "''" + } +} + impl<'a> From<&'a str> for Trivia<'a> { fn from(value: &'a str) -> Self { // Remove any prefixes (e.g., remove `u` from `u"foo"`). @@ -231,12 +241,35 @@ impl<'a> From<&'a str> for Trivia<'a> { } } +fn text_ends_at_quote(locator: &Locator, range: TextRange, settings: &LinterSettings) -> bool { + let trivia_of_next_char: Trivia = locator + .slice(TextRange::new( + range.end(), + TextSize::new(range.end().to_u32() + 1), + )) + .into(); + trivia_of_next_char + .raw_text + .contains(good_docstring(settings.flake8_quotes.docstring_quotes)) +} + /// Q002 fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> Option { let quotes_settings = &settings.flake8_quotes; let text = locator.slice(range); let trivia: Trivia = text.into(); + if trivia.has_empty_text() && text_ends_at_quote(locator, range, settings) { + // Cannot fix. Fix would result in an one-sided multi-line docstring, + // which would introduce an error. + let diagnostic = Diagnostic::new( + BadQuotesDocstring { + preferred_quote: quotes_settings.multiline_quotes, + }, + range, + ); + return Some(diagnostic); + } if trivia .raw_text @@ -344,6 +377,18 @@ fn strings( // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { + if trivia.has_empty_text() && text_ends_at_quote(locator, *range, settings) { + // Cannot fix. Fix would result in an one-sided multi-line docstring, + // which would introduce an error. + let diagnostic = Diagnostic::new( + BadQuotesInlineString { + preferred_quote: quotes_settings.inline_quotes, + }, + *range, + ); + diagnostics.push(diagnostic); + continue; + } let mut diagnostic = Diagnostic::new( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes, diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap new file mode 100644 index 0000000000000..db82003de32f2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 151 +--- +docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Double quote docstring found but single quotes preferred + | +1 | ''"Start with empty string" ' and lint docstring safely' + | ^^ Q002 +2 | +3 | def foo(): + | + = help: Replace double quotes docstring with single quotes + +docstring_singles_mixed_quotes_module_singleline_var_1.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred + | +3 | def foo(): +4 | pass +5 | """ this is not a docstring """ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q001 + | + = help: Replace double multiline quotes with single quotes + +ℹ Safe fix +2 2 | +3 3 | def foo(): +4 4 | pass +5 |-""" this is not a docstring """ + 5 |+''' this is not a docstring ''' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap new file mode 100644 index 0000000000000..08c1169d539f7 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 151 +--- +docstring_singles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Single quote docstring found but double quotes preferred + | +1 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^ Q002 +2 | +3 | def foo(): + | + = help: Replace single quotes docstring with double quotes + +ℹ Safe fix +1 |-'Do not'" start with empty string" ' and lint docstring safely' + 1 |+"Do not"" start with empty string" ' and lint docstring safely' +2 2 | +3 3 | def foo(): +4 4 | pass + +docstring_singles_mixed_quotes_module_singleline_var_2.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred + | +3 | def foo(): +4 | pass +5 | """ this is not a docstring """ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q001 + | + = help: Replace double multiline quotes with single quotes + +ℹ Safe fix +2 2 | +3 3 | def foo(): +4 4 | pass +5 |-""" this is not a docstring """ + 5 |+''' this is not a docstring ''' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap new file mode 100644 index 0000000000000..a941a1a9bc4e0 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 186 +--- +docstring_doubles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote docstring found but double quotes preferred + | +1 | ""'Start with empty string' ' and lint docstring safely' + | ^^ Q002 +2 | +3 | def foo(): + | + = help: Replace single quotes docstring with double quotes diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap new file mode 100644 index 0000000000000..8993a4b7248bb --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 186 +--- +docstring_doubles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Double quote docstring found but single quotes preferred + | +1 | "Do not"' start with empty string' ' and lint docstring safely' + | ^^^^^^^^ Q002 +2 | +3 | def foo(): + | + = help: Replace double quotes docstring with single quotes + +ℹ Safe fix +1 |-"Do not"' start with empty string' ' and lint docstring safely' + 1 |+'Do not'' start with empty string' ' and lint docstring safely' +2 2 | +3 3 | def foo(): +4 4 | pass From 0806412f591b17977ab022139ac6ca9fbd8f2135 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sun, 3 Mar 2024 13:35:23 +0100 Subject: [PATCH 04/13] Add tests for inline strings --- .../doubles_would_be_triple_quotes.py | 2 + .../singles_would_be_triple_quotes.py | 2 + .../src/rules/flake8_quotes/mod.rs | 2 + ...ver_singles_would_be_triple_quotes.py.snap | 50 +++++++++++++++++++ ...ver_doubles_would_be_triple_quotes.py.snap | 24 +++++++++ 5 files changed, 80 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_would_be_triple_quotes.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_would_be_triple_quotes.py create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_would_be_triple_quotes.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_would_be_triple_quotes.py new file mode 100644 index 0000000000000..49dcb2d53b189 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_would_be_triple_quotes.py @@ -0,0 +1,2 @@ +s = ""'Start with empty string' ' and lint docstring safely' +s = "Do not"' start with empty string' ' and lint docstring safely' diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_would_be_triple_quotes.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_would_be_triple_quotes.py new file mode 100644 index 0000000000000..69b396dd7e097 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_would_be_triple_quotes.py @@ -0,0 +1,2 @@ +s = ''"Start with empty string" ' and lint docstring safely' +s = 'Do not'" start with empty string" ' and lint docstring safely' diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 1b2ff3770c158..a7093e8b96f03 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -24,6 +24,7 @@ mod tests { #[test_case(Path::new("doubles_multiline_string.py"))] #[test_case(Path::new("doubles_noqa.py"))] #[test_case(Path::new("doubles_wrapped.py"))] + #[test_case(Path::new("doubles_would_be_triple_quotes.py"))] fn require_singles(path: &Path) -> Result<()> { let snapshot = format!("require_singles_over_{}", path.to_string_lossy()); let diagnostics = test_path( @@ -93,6 +94,7 @@ mod tests { #[test_case(Path::new("singles_multiline_string.py"))] #[test_case(Path::new("singles_noqa.py"))] #[test_case(Path::new("singles_wrapped.py"))] + #[test_case(Path::new("singles_would_be_triple_quotes.py"))] fn require_doubles(path: &Path) -> Result<()> { let snapshot = format!("require_doubles_over_{}", path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap new file mode 100644 index 0000000000000..63eb8d375a35a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap @@ -0,0 +1,50 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 118 +--- +singles_would_be_triple_quotes.py:1:5: Q000 Single quotes found but double quotes preferred + | +1 | s = ''"Start with empty string" ' and lint docstring safely' + | ^^ Q000 +2 | s = 'Do not'" start with empty string" ' and lint docstring safely' + | + = help: Replace single quotes with double quotes + +singles_would_be_triple_quotes.py:1:33: Q000 [*] Single quotes found but double quotes preferred + | +1 | s = ''"Start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +2 | s = 'Do not'" start with empty string" ' and lint docstring safely' + | + = help: Replace single quotes with double quotes + +ℹ Safe fix +1 |-s = ''"Start with empty string" ' and lint docstring safely' + 1 |+s = ''"Start with empty string" " and lint docstring safely" +2 2 | s = 'Do not'" start with empty string" ' and lint docstring safely' + +singles_would_be_triple_quotes.py:2:5: Q000 [*] Single quotes found but double quotes preferred + | +1 | s = ''"Start with empty string" ' and lint docstring safely' +2 | s = 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^ Q000 + | + = help: Replace single quotes with double quotes + +ℹ Safe fix +1 1 | s = ''"Start with empty string" ' and lint docstring safely' +2 |-s = 'Do not'" start with empty string" ' and lint docstring safely' + 2 |+s = "Do not"" start with empty string" ' and lint docstring safely' + +singles_would_be_triple_quotes.py:2:40: Q000 [*] Single quotes found but double quotes preferred + | +1 | s = ''"Start with empty string" ' and lint docstring safely' +2 | s = 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 + | + = help: Replace single quotes with double quotes + +ℹ Safe fix +1 1 | s = ''"Start with empty string" ' and lint docstring safely' +2 |-s = 'Do not'" start with empty string" ' and lint docstring safely' + 2 |+s = 'Do not'" start with empty string" " and lint docstring safely" diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap new file mode 100644 index 0000000000000..99486ba4c70a7 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 48 +--- +doubles_would_be_triple_quotes.py:1:5: Q000 Double quotes found but single quotes preferred + | +1 | s = ""'Start with empty string' ' and lint docstring safely' + | ^^ Q000 +2 | s = "Do not"' start with empty string' ' and lint docstring safely' + | + = help: Replace double quotes with single quotes + +doubles_would_be_triple_quotes.py:2:5: Q000 [*] Double quotes found but single quotes preferred + | +1 | s = ""'Start with empty string' ' and lint docstring safely' +2 | s = "Do not"' start with empty string' ' and lint docstring safely' + | ^^^^^^^^ Q000 + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +1 1 | s = ""'Start with empty string' ' and lint docstring safely' +2 |-s = "Do not"' start with empty string' ' and lint docstring safely' + 2 |+s = 'Do not'' start with empty string' ' and lint docstring safely' From 0ebeb2df93a5eea13a56089e1fa1dcef91469cb6 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 14:22:43 +0100 Subject: [PATCH 05/13] Fix diagnostic --- .../flake8_quotes/rules/check_string_quotes.rs | 14 +++++++++----- ...es_mixed_quotes_module_singleline_var_1.py.snap | 6 +++--- ...es_mixed_quotes_module_singleline_var_1.py.snap | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index d15221ea04239..c3c06483e2daf 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -241,7 +241,7 @@ impl<'a> From<&'a str> for Trivia<'a> { } } -fn text_ends_at_quote(locator: &Locator, range: TextRange, settings: &LinterSettings) -> bool { +fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: &Quote) -> bool { let trivia_of_next_char: Trivia = locator .slice(TextRange::new( range.end(), @@ -250,7 +250,7 @@ fn text_ends_at_quote(locator: &Locator, range: TextRange, settings: &LinterSett .into(); trivia_of_next_char .raw_text - .contains(good_docstring(settings.flake8_quotes.docstring_quotes)) + .contains(good_docstring(*quote)) } /// Q002 @@ -259,12 +259,14 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> let text = locator.slice(range); let trivia: Trivia = text.into(); - if trivia.has_empty_text() && text_ends_at_quote(locator, range, settings) { + if trivia.has_empty_text() + && text_ends_at_quote(locator, range, &settings.flake8_quotes.docstring_quotes) + { // Cannot fix. Fix would result in an one-sided multi-line docstring, // which would introduce an error. let diagnostic = Diagnostic::new( BadQuotesDocstring { - preferred_quote: quotes_settings.multiline_quotes, + preferred_quote: quotes_settings.docstring_quotes, }, range, ); @@ -377,7 +379,9 @@ fn strings( // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { - if trivia.has_empty_text() && text_ends_at_quote(locator, *range, settings) { + if trivia.has_empty_text() + && text_ends_at_quote(locator, *range, &settings.flake8_quotes.inline_quotes) + { // Cannot fix. Fix would result in an one-sided multi-line docstring, // which would introduce an error. let diagnostic = Diagnostic::new( diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap index db82003de32f2..a3b38ffdd00d7 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap @@ -1,15 +1,15 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 151 +assertion_line: 153 --- -docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Double quote docstring found but single quotes preferred +docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote docstring found but double quotes preferred | 1 | ''"Start with empty string" ' and lint docstring safely' | ^^ Q002 2 | 3 | def foo(): | - = help: Replace double quotes docstring with single quotes + = help: Replace single quotes docstring with double quotes docstring_singles_mixed_quotes_module_singleline_var_1.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap index a941a1a9bc4e0..912ee8e09f0d4 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap @@ -1,12 +1,12 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 186 +assertion_line: 190 --- -docstring_doubles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote docstring found but double quotes preferred +docstring_doubles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Double quote docstring found but single quotes preferred | 1 | ""'Start with empty string' ' and lint docstring safely' | ^^ Q002 2 | 3 | def foo(): | - = help: Replace single quotes docstring with double quotes + = help: Replace double quotes docstring with single quotes From 7c6893195bcdf6b35cbaa57d57dcad1e4481a276 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 14:34:05 +0100 Subject: [PATCH 06/13] Add further docstring tests --- ...string_doubles_mixed_quotes_class_var_1.py | 9 +++ ...string_doubles_mixed_quotes_class_var_2.py | 9 +++ ...string_singles_mixed_quotes_class_var_1.py | 9 +++ ...string_singles_mixed_quotes_class_var_2.py | 9 +++ .../src/rules/flake8_quotes/mod.rs | 4 ++ ...g_singles_mixed_quotes_class_var_1.py.snap | 30 ++++++++++ ...g_singles_mixed_quotes_class_var_2.py.snap | 55 +++++++++++++++++++ ...g_doubles_mixed_quotes_class_var_1.py.snap | 30 ++++++++++ ...g_doubles_mixed_quotes_class_var_2.py.snap | 55 +++++++++++++++++++ 9 files changed, 210 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_1.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_2.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_1.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_2.py create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_1.py new file mode 100644 index 0000000000000..b29dd5d6d9db4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_1.py @@ -0,0 +1,9 @@ +class SingleLineDocstrings(): + ""'Start with empty string' ' and lint docstring safely' + """ Not a docstring """ + + def foo(self, bar="""not a docstring"""): + ""'Start with empty string' ' and lint docstring safely' + pass + + class Nested(foo()[:]): ""'Start with empty string' ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_2.py new file mode 100644 index 0000000000000..813e87df2227c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_doubles_mixed_quotes_class_var_2.py @@ -0,0 +1,9 @@ +class SingleLineDocstrings(): + "Do not"' start with empty string' ' and lint docstring safely' + """ Not a docstring """ + + def foo(self, bar="""not a docstring"""): + "Do not"' start with empty string' ' and lint docstring safely' + pass + + class Nested(foo()[:]): "Do not"' start with empty string' ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_1.py new file mode 100644 index 0000000000000..beaa3f1ac71fb --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_1.py @@ -0,0 +1,9 @@ +class SingleLineDocstrings(): + ''"Start with empty string" ' and lint docstring safely' + ''' Not a docstring ''' + + def foo(self, bar='''not a docstring'''): + ''"Start with empty string" ' and lint docstring safely' + pass + + class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_2.py new file mode 100644 index 0000000000000..d58df0eaa7c57 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/docstring_singles_mixed_quotes_class_var_2.py @@ -0,0 +1,9 @@ +class SingleLineDocstrings(): + 'Do not'" start with empty string" ' and lint docstring safely' + ''' Not a docstring ''' + + def foo(self, bar='''not a docstring'''): + 'Do not'" start with empty string" ' and lint docstring safely' + pass + + class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index a7093e8b96f03..25de4f72ec880 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -131,6 +131,8 @@ mod tests { #[test_case(Path::new("docstring_singles_function.py"))] #[test_case(Path::new("docstring_singles_mixed_quotes_module_singleline_var_1.py"))] #[test_case(Path::new("docstring_singles_mixed_quotes_module_singleline_var_2.py"))] + #[test_case(Path::new("docstring_singles_mixed_quotes_class_var_1.py"))] + #[test_case(Path::new("docstring_singles_mixed_quotes_class_var_2.py"))] fn require_docstring_doubles(path: &Path) -> Result<()> { let snapshot = format!("require_docstring_doubles_over_{}", path.to_string_lossy()); let diagnostics = test_path( @@ -166,6 +168,8 @@ mod tests { #[test_case(Path::new("docstring_singles_function.py"))] #[test_case(Path::new("docstring_doubles_mixed_quotes_module_singleline_var_1.py"))] #[test_case(Path::new("docstring_doubles_mixed_quotes_module_singleline_var_2.py"))] + #[test_case(Path::new("docstring_doubles_mixed_quotes_class_var_1.py"))] + #[test_case(Path::new("docstring_doubles_mixed_quotes_class_var_2.py"))] fn require_docstring_singles(path: &Path) -> Result<()> { let snapshot = format!("require_docstring_singles_over_{}", path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap new file mode 100644 index 0000000000000..416c1a92050cc --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 155 +--- +docstring_singles_mixed_quotes_class_var_1.py:2:5: Q002 Single quote docstring found but double quotes preferred + | +1 | class SingleLineDocstrings(): +2 | ''"Start with empty string" ' and lint docstring safely' + | ^^ Q002 +3 | ''' Not a docstring ''' + | + = help: Replace single quotes docstring with double quotes + +docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring found but double quotes preferred + | +5 | def foo(self, bar='''not a docstring'''): +6 | ''"Start with empty string" ' and lint docstring safely' + | ^^ Q002 +7 | pass + | + = help: Replace single quotes docstring with double quotes + +docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring found but double quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass + | ^^ Q002 + | + = help: Replace single quotes docstring with double quotes diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap new file mode 100644 index 0000000000000..585903dea2705 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 155 +--- +docstring_singles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Single quote docstring found but double quotes preferred + | +1 | class SingleLineDocstrings(): +2 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^ Q002 +3 | ''' Not a docstring ''' + | + = help: Replace single quotes docstring with double quotes + +ℹ Safe fix +1 1 | class SingleLineDocstrings(): +2 |- 'Do not'" start with empty string" ' and lint docstring safely' + 2 |+ "Do not"" start with empty string" ' and lint docstring safely' +3 3 | ''' Not a docstring ''' +4 4 | +5 5 | def foo(self, bar='''not a docstring'''): + +docstring_singles_mixed_quotes_class_var_2.py:6:9: Q002 [*] Single quote docstring found but double quotes preferred + | +5 | def foo(self, bar='''not a docstring'''): +6 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^ Q002 +7 | pass + | + = help: Replace single quotes docstring with double quotes + +ℹ Safe fix +3 3 | ''' Not a docstring ''' +4 4 | +5 5 | def foo(self, bar='''not a docstring'''): +6 |- 'Do not'" start with empty string" ' and lint docstring safely' + 6 |+ "Do not"" start with empty string" ' and lint docstring safely' +7 7 | pass +8 8 | +9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + +docstring_singles_mixed_quotes_class_var_2.py:9:29: Q002 [*] Single quote docstring found but double quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + | ^^^^^^^^ Q002 + | + = help: Replace single quotes docstring with double quotes + +ℹ Safe fix +6 6 | 'Do not'" start with empty string" ' and lint docstring safely' +7 7 | pass +8 8 | +9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + 9 |+ class Nested(foo()[:]): "Do not"" start with empty string" ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap new file mode 100644 index 0000000000000..d9a6057628594 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 190 +--- +docstring_doubles_mixed_quotes_class_var_1.py:2:5: Q002 Double quote docstring found but single quotes preferred + | +1 | class SingleLineDocstrings(): +2 | ""'Start with empty string' ' and lint docstring safely' + | ^^ Q002 +3 | """ Not a docstring """ + | + = help: Replace double quotes docstring with single quotes + +docstring_doubles_mixed_quotes_class_var_1.py:6:9: Q002 Double quote docstring found but single quotes preferred + | +5 | def foo(self, bar="""not a docstring"""): +6 | ""'Start with empty string' ' and lint docstring safely' + | ^^ Q002 +7 | pass + | + = help: Replace double quotes docstring with single quotes + +docstring_doubles_mixed_quotes_class_var_1.py:9:29: Q002 Double quote docstring found but single quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): ""'Start with empty string' ' and lint docstring safely'; pass + | ^^ Q002 + | + = help: Replace double quotes docstring with single quotes diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap new file mode 100644 index 0000000000000..78e907fb7f59b --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +assertion_line: 190 +--- +docstring_doubles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Double quote docstring found but single quotes preferred + | +1 | class SingleLineDocstrings(): +2 | "Do not"' start with empty string' ' and lint docstring safely' + | ^^^^^^^^ Q002 +3 | """ Not a docstring """ + | + = help: Replace double quotes docstring with single quotes + +ℹ Safe fix +1 1 | class SingleLineDocstrings(): +2 |- "Do not"' start with empty string' ' and lint docstring safely' + 2 |+ 'Do not'' start with empty string' ' and lint docstring safely' +3 3 | """ Not a docstring """ +4 4 | +5 5 | def foo(self, bar="""not a docstring"""): + +docstring_doubles_mixed_quotes_class_var_2.py:6:9: Q002 [*] Double quote docstring found but single quotes preferred + | +5 | def foo(self, bar="""not a docstring"""): +6 | "Do not"' start with empty string' ' and lint docstring safely' + | ^^^^^^^^ Q002 +7 | pass + | + = help: Replace double quotes docstring with single quotes + +ℹ Safe fix +3 3 | """ Not a docstring """ +4 4 | +5 5 | def foo(self, bar="""not a docstring"""): +6 |- "Do not"' start with empty string' ' and lint docstring safely' + 6 |+ 'Do not'' start with empty string' ' and lint docstring safely' +7 7 | pass +8 8 | +9 9 | class Nested(foo()[:]): "Do not"' start with empty string' ' and lint docstring safely'; pass + +docstring_doubles_mixed_quotes_class_var_2.py:9:29: Q002 [*] Double quote docstring found but single quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): "Do not"' start with empty string' ' and lint docstring safely'; pass + | ^^^^^^^^ Q002 + | + = help: Replace double quotes docstring with single quotes + +ℹ Safe fix +6 6 | "Do not"' start with empty string' ' and lint docstring safely' +7 7 | pass +8 8 | +9 |- class Nested(foo()[:]): "Do not"' start with empty string' ' and lint docstring safely'; pass + 9 |+ class Nested(foo()[:]): 'Do not'' start with empty string' ' and lint docstring safely'; pass From f84ded8dc0581996d8d2829a1e13b5b8d7fd7d9a Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 14:35:13 +0100 Subject: [PATCH 07/13] Revert "Deactivate Q000" This reverts commit e65d99f381d7524d3bfd8b2bce3b6d156199071a. --- crates/ruff_linter/src/rules/flake8_quotes/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 25de4f72ec880..7ecfbd2d375fb 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -145,6 +145,7 @@ mod tests { avoid_escape: true, }, ..LinterSettings::for_rules(vec![ + Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, @@ -182,6 +183,7 @@ mod tests { avoid_escape: true, }, ..LinterSettings::for_rules(vec![ + Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, Rule::AvoidableEscapedQuote, From 402a08388bb8e418e3a585caf84c43d4eeb49681 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 15:26:40 +0100 Subject: [PATCH 08/13] Fix autofix error of Q000 --- .../rules/check_string_quotes.rs | 20 ++++++- ...g_singles_mixed_quotes_class_var_1.py.snap | 29 +++++++++- ...g_singles_mixed_quotes_class_var_2.py.snap | 54 ++++++++++++++++++- ...xed_quotes_module_singleline_var_1.py.snap | 11 +++- ...xed_quotes_module_singleline_var_2.py.snap | 18 ++++++- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index c3c06483e2daf..73413a976ec79 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -241,6 +241,17 @@ impl<'a> From<&'a str> for Trivia<'a> { } } +fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: &Quote) -> bool { + let trivia_of_previous_char: Trivia = locator + .slice(TextRange::new( + TextSize::new(range.start().to_u32() - 2), + range.start(), + )) + .into(); + let pat = format!("{}{}", good_docstring(*quote), good_docstring(*quote)); + trivia_of_previous_char.raw_text.contains(&pat) +} + fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: &Quote) -> bool { let trivia_of_next_char: Trivia = locator .slice(TextRange::new( @@ -379,8 +390,13 @@ fn strings( // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { - if trivia.has_empty_text() - && text_ends_at_quote(locator, *range, &settings.flake8_quotes.inline_quotes) + if (trivia.has_empty_text() + && text_ends_at_quote(locator, *range, &settings.flake8_quotes.inline_quotes)) + || text_starts_at_double_quote( + locator, + *range, + &settings.flake8_quotes.inline_quotes, + ) { // Cannot fix. Fix would result in an one-sided multi-line docstring, // which would introduce an error. diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap index 416c1a92050cc..e1f3350bb6621 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 155 +assertion_line: 156 --- docstring_singles_mixed_quotes_class_var_1.py:2:5: Q002 Single quote docstring found but double quotes preferred | @@ -11,6 +11,15 @@ docstring_singles_mixed_quotes_class_var_1.py:2:5: Q002 Single quote docstring f | = help: Replace single quotes docstring with double quotes +docstring_singles_mixed_quotes_class_var_1.py:2:7: Q000 Double quotes found but single quotes preferred + | +1 | class SingleLineDocstrings(): +2 | ''"Start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +3 | ''' Not a docstring ''' + | + = help: Replace double quotes with single quotes + docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring found but double quotes preferred | 5 | def foo(self, bar='''not a docstring'''): @@ -20,6 +29,15 @@ docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring f | = help: Replace single quotes docstring with double quotes +docstring_singles_mixed_quotes_class_var_1.py:6:11: Q000 Double quotes found but single quotes preferred + | +5 | def foo(self, bar='''not a docstring'''): +6 | ''"Start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +7 | pass + | + = help: Replace double quotes with single quotes + docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring found but double quotes preferred | 7 | pass @@ -28,3 +46,12 @@ docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring | ^^ Q002 | = help: Replace single quotes docstring with double quotes + +docstring_singles_mixed_quotes_class_var_1.py:9:31: Q000 Double quotes found but single quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 + | + = help: Replace double quotes with single quotes diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap index 585903dea2705..75768475ad515 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 155 +assertion_line: 156 --- docstring_singles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Single quote docstring found but double quotes preferred | @@ -19,6 +19,23 @@ docstring_singles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Single quote docstri 4 4 | 5 5 | def foo(self, bar='''not a docstring'''): +docstring_singles_mixed_quotes_class_var_2.py:2:13: Q000 [*] Double quotes found but single quotes preferred + | +1 | class SingleLineDocstrings(): +2 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +3 | ''' Not a docstring ''' + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +1 1 | class SingleLineDocstrings(): +2 |- 'Do not'" start with empty string" ' and lint docstring safely' + 2 |+ 'Do not'' start with empty string' ' and lint docstring safely' +3 3 | ''' Not a docstring ''' +4 4 | +5 5 | def foo(self, bar='''not a docstring'''): + docstring_singles_mixed_quotes_class_var_2.py:6:9: Q002 [*] Single quote docstring found but double quotes preferred | 5 | def foo(self, bar='''not a docstring'''): @@ -38,6 +55,25 @@ docstring_singles_mixed_quotes_class_var_2.py:6:9: Q002 [*] Single quote docstri 8 8 | 9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass +docstring_singles_mixed_quotes_class_var_2.py:6:17: Q000 [*] Double quotes found but single quotes preferred + | +5 | def foo(self, bar='''not a docstring'''): +6 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +7 | pass + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +3 3 | ''' Not a docstring ''' +4 4 | +5 5 | def foo(self, bar='''not a docstring'''): +6 |- 'Do not'" start with empty string" ' and lint docstring safely' + 6 |+ 'Do not'' start with empty string' ' and lint docstring safely' +7 7 | pass +8 8 | +9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + docstring_singles_mixed_quotes_class_var_2.py:9:29: Q002 [*] Single quote docstring found but double quotes preferred | 7 | pass @@ -53,3 +89,19 @@ docstring_singles_mixed_quotes_class_var_2.py:9:29: Q002 [*] Single quote docstr 8 8 | 9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 |+ class Nested(foo()[:]): "Do not"" start with empty string" ' and lint docstring safely'; pass + +docstring_singles_mixed_quotes_class_var_2.py:9:37: Q000 [*] Double quotes found but single quotes preferred + | +7 | pass +8 | +9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +6 6 | 'Do not'" start with empty string" ' and lint docstring safely' +7 7 | pass +8 8 | +9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass + 9 |+ class Nested(foo()[:]): 'Do not'' start with empty string' ' and lint docstring safely'; pass diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap index a3b38ffdd00d7..e65c3c7f421a0 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 153 +assertion_line: 156 --- docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote docstring found but double quotes preferred | @@ -11,6 +11,15 @@ docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote | = help: Replace single quotes docstring with double quotes +docstring_singles_mixed_quotes_module_singleline_var_1.py:1:3: Q000 Double quotes found but single quotes preferred + | +1 | ''"Start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +2 | +3 | def foo(): + | + = help: Replace double quotes with single quotes + docstring_singles_mixed_quotes_module_singleline_var_1.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred | 3 | def foo(): diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap index 08c1169d539f7..f365eb0534e59 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 151 +assertion_line: 156 --- docstring_singles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Single quote docstring found but double quotes preferred | @@ -18,6 +18,22 @@ docstring_singles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Single q 3 3 | def foo(): 4 4 | pass +docstring_singles_mixed_quotes_module_singleline_var_2.py:1:9: Q000 [*] Double quotes found but single quotes preferred + | +1 | 'Do not'" start with empty string" ' and lint docstring safely' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 +2 | +3 | def foo(): + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +1 |-'Do not'" start with empty string" ' and lint docstring safely' + 1 |+'Do not'' start with empty string' ' and lint docstring safely' +2 2 | +3 3 | def foo(): +4 4 | pass + docstring_singles_mixed_quotes_module_singleline_var_2.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred | 3 | def foo(): From 4cef19cb249da0b696c044b783e571a89e3ee68e Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 15:29:53 +0100 Subject: [PATCH 09/13] Remove assertion line in snap file --- ...ubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap | 1 - ...ubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap | 1 - ...ocstring_singles_mixed_quotes_module_singleline_var_1.py.snap | 1 - ...ocstring_singles_mixed_quotes_module_singleline_var_2.py.snap | 1 - ...ngles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap | 1 - ...ngles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap | 1 - ...ocstring_doubles_mixed_quotes_module_singleline_var_1.py.snap | 1 - ...ocstring_doubles_mixed_quotes_module_singleline_var_2.py.snap | 1 - ...__require_doubles_over_singles_would_be_triple_quotes.py.snap | 1 - ...__require_singles_over_doubles_would_be_triple_quotes.py.snap | 1 - 10 files changed, 10 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap index e1f3350bb6621..ead01a887e0ca 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_1.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 156 --- docstring_singles_mixed_quotes_class_var_1.py:2:5: Q002 Single quote docstring found but double quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap index 75768475ad515..67203cf8ff337 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_class_var_2.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 156 --- docstring_singles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Single quote docstring found but double quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap index e65c3c7f421a0..b12ef7e5b6e9c 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_1.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 156 --- docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote docstring found but double quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap index f365eb0534e59..a0f9cc158c40a 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_mixed_quotes_module_singleline_var_2.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 156 --- docstring_singles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Single quote docstring found but double quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap index d9a6057628594..96ccdbd7f484f 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_1.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 190 --- docstring_doubles_mixed_quotes_class_var_1.py:2:5: Q002 Double quote docstring found but single quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap index 78e907fb7f59b..e02c3c17c1e3e 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_class_var_2.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 190 --- docstring_doubles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Double quote docstring found but single quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap index 912ee8e09f0d4..df92925a9bd38 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_1.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 190 --- docstring_doubles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Double quote docstring found but single quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap index 8993a4b7248bb..31efd169aff73 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_mixed_quotes_module_singleline_var_2.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 186 --- docstring_doubles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Double quote docstring found but single quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap index 63eb8d375a35a..3c5b35cd44202 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_would_be_triple_quotes.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 118 --- singles_would_be_triple_quotes.py:1:5: Q000 Single quotes found but double quotes preferred | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap index 99486ba4c70a7..031164bad78ba 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_would_be_triple_quotes.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -assertion_line: 48 --- doubles_would_be_triple_quotes.py:1:5: Q000 Double quotes found but single quotes preferred | From bf569645cf9289983e4f9c007eb1d7b88c272ee3 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 15:36:25 +0100 Subject: [PATCH 10/13] Use pass by value --- .../flake8_quotes/rules/check_string_quotes.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 73413a976ec79..0de07374e269b 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -241,27 +241,25 @@ impl<'a> From<&'a str> for Trivia<'a> { } } -fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: &Quote) -> bool { +fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { let trivia_of_previous_char: Trivia = locator .slice(TextRange::new( TextSize::new(range.start().to_u32() - 2), range.start(), )) .into(); - let pat = format!("{}{}", good_docstring(*quote), good_docstring(*quote)); + let pat = format!("{}{}", good_docstring(quote), good_docstring(quote)); trivia_of_previous_char.raw_text.contains(&pat) } -fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: &Quote) -> bool { +fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { let trivia_of_next_char: Trivia = locator .slice(TextRange::new( range.end(), TextSize::new(range.end().to_u32() + 1), )) .into(); - trivia_of_next_char - .raw_text - .contains(good_docstring(*quote)) + trivia_of_next_char.raw_text.contains(good_docstring(quote)) } /// Q002 @@ -271,7 +269,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> let text = locator.slice(range); let trivia: Trivia = text.into(); if trivia.has_empty_text() - && text_ends_at_quote(locator, range, &settings.flake8_quotes.docstring_quotes) + && text_ends_at_quote(locator, range, settings.flake8_quotes.docstring_quotes) { // Cannot fix. Fix would result in an one-sided multi-line docstring, // which would introduce an error. @@ -391,11 +389,11 @@ fn strings( && !relax_quote { if (trivia.has_empty_text() - && text_ends_at_quote(locator, *range, &settings.flake8_quotes.inline_quotes)) + && text_ends_at_quote(locator, *range, settings.flake8_quotes.inline_quotes)) || text_starts_at_double_quote( locator, *range, - &settings.flake8_quotes.inline_quotes, + settings.flake8_quotes.inline_quotes, ) { // Cannot fix. Fix would result in an one-sided multi-line docstring, From 307c06aae11a6ebf6ffd4db55c77edb63038e777 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 16:03:11 +0100 Subject: [PATCH 11/13] Work on text instead of on Trivia --- .../rules/check_string_quotes.rs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 0de07374e269b..e5fe4f4bc70cd 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -242,24 +242,20 @@ impl<'a> From<&'a str> for Trivia<'a> { } fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let trivia_of_previous_char: Trivia = locator - .slice(TextRange::new( - TextSize::new(range.start().to_u32() - 2), - range.start(), - )) - .into(); + let raw_text_of_previous_two_chars = locator.slice(TextRange::new( + TextSize::new(range.start().to_u32() - 2), + range.start(), + )); let pat = format!("{}{}", good_docstring(quote), good_docstring(quote)); - trivia_of_previous_char.raw_text.contains(&pat) + raw_text_of_previous_two_chars.contains(&pat) } fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let trivia_of_next_char: Trivia = locator - .slice(TextRange::new( - range.end(), - TextSize::new(range.end().to_u32() + 1), - )) - .into(); - trivia_of_next_char.raw_text.contains(good_docstring(quote)) + let raw_text_of_next_char = locator.slice(TextRange::new( + range.end(), + TextSize::new(range.end().to_u32() + 1), + )); + raw_text_of_next_char.contains(good_docstring(quote)) } /// Q002 From 94eff14701841518ced8f4c6c192137b98e9f824 Mon Sep 17 00:00:00 2001 From: Robin Caloudis Date: Sat, 16 Mar 2024 16:43:37 +0100 Subject: [PATCH 12/13] Remove unnecessary allocation --- .../rules/flake8_quotes/rules/check_string_quotes.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index e5fe4f4bc70cd..5df89820abbce 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -242,20 +242,20 @@ impl<'a> From<&'a str> for Trivia<'a> { } fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let raw_text_of_previous_two_chars = locator.slice(TextRange::new( + let previous_two_chars = locator.slice(TextRange::new( TextSize::new(range.start().to_u32() - 2), range.start(), )); - let pat = format!("{}{}", good_docstring(quote), good_docstring(quote)); - raw_text_of_previous_two_chars.contains(&pat) + &previous_two_chars[0..1] == good_docstring(quote) + && &previous_two_chars[1..2] == good_docstring(quote) } fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let raw_text_of_next_char = locator.slice(TextRange::new( + let next_char = locator.slice(TextRange::new( range.end(), TextSize::new(range.end().to_u32() + 1), )); - raw_text_of_next_char.contains(good_docstring(quote)) + &next_char[0..1] == good_docstring(quote) } /// Q002 From f4da30cece1da1d6ae1f3a679315a310e3b55894 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 17 Mar 2024 21:12:39 -0400 Subject: [PATCH 13/13] Tweak some names and documentation --- .../rules/check_string_quotes.rs | 81 +++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index b4ba5a557a6b5..449fdcfd2feef 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -192,10 +192,10 @@ const fn good_multiline_ending(quote: Quote) -> &'static str { } } -const fn good_docstring(quote: Quote) -> &'static str { +const fn good_docstring(quote: Quote) -> char { match quote { - Quote::Double => "\"", - Quote::Single => "'", + Quote::Double => '"', + Quote::Single => '\'', } } @@ -241,21 +241,18 @@ impl<'a> From<&'a str> for Trivia<'a> { } } -fn text_starts_at_double_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let previous_two_chars = locator.slice(TextRange::new( - TextSize::new(range.start().to_u32() - 2), - range.start(), - )); - &previous_two_chars[0..1] == good_docstring(quote) - && &previous_two_chars[1..2] == good_docstring(quote) +/// Returns `true` if the [`TextRange`] is preceded by two consecutive quotes. +fn text_starts_at_consecutive_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { + let mut previous_two_chars = locator.up_to(range.start()).chars().rev(); + previous_two_chars.next() == Some(good_docstring(quote)) + && previous_two_chars.next() == Some(good_docstring(quote)) } +/// Returns `true` if the [`TextRange`] ends at a quote character. fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool { - let next_char = locator.slice(TextRange::new( - range.end(), - TextSize::new(range.end().to_u32() + 1), - )); - &next_char[0..1] == good_docstring(quote) + locator + .after(range.end()) + .starts_with(good_docstring(quote)) } /// Q002 @@ -267,15 +264,14 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> if trivia.has_empty_text() && text_ends_at_quote(locator, range, settings.flake8_quotes.docstring_quotes) { - // Cannot fix. Fix would result in an one-sided multi-line docstring, - // which would introduce an error. - let diagnostic = Diagnostic::new( + // Fixing this would result in a one-sided multi-line docstring, which would + // introduce a syntax error. + return Some(Diagnostic::new( BadQuotesDocstring { preferred_quote: quotes_settings.docstring_quotes, }, range, - ); - return Some(diagnostic); + )); } if trivia @@ -293,7 +289,9 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> ); let quote_count = if trivia.is_multiline { 3 } else { 1 }; let string_contents = &trivia.raw_text[quote_count..trivia.raw_text.len() - quote_count]; - let quote = good_docstring(quotes_settings.docstring_quotes).repeat(quote_count); + let quote = good_docstring(quotes_settings.docstring_quotes) + .to_string() + .repeat(quote_count); let mut fixed_contents = String::with_capacity(trivia.prefix.len() + string_contents.len() + quote.len() * 2); fixed_contents.push_str(trivia.prefix); @@ -384,25 +382,42 @@ fn strings( // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { - if (trivia.has_empty_text() - && text_ends_at_quote(locator, *range, settings.flake8_quotes.inline_quotes)) - || text_starts_at_double_quote( - locator, - *range, - settings.flake8_quotes.inline_quotes, - ) + if trivia.has_empty_text() + && text_ends_at_quote(locator, *range, settings.flake8_quotes.inline_quotes) { - // Cannot fix. Fix would result in an one-sided multi-line docstring, - // which would introduce an error. - let diagnostic = Diagnostic::new( + // Fixing this would introduce a syntax error. For example, changing the initial + // single quotes to double quotes would result in a syntax error: + // ```python + // ''"assert" ' SAM macro definitions ''' + // ``` + diagnostics.push(Diagnostic::new( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes, }, *range, - ); - diagnostics.push(diagnostic); + )); continue; } + + if text_starts_at_consecutive_quote( + locator, + *range, + settings.flake8_quotes.inline_quotes, + ) { + // Fixing this would introduce a syntax error. For example, changing the double + // doubles to single quotes would result in a syntax error: + // ```python + // ''"assert" ' SAM macro definitions ''' + // ``` + diagnostics.push(Diagnostic::new( + BadQuotesInlineString { + preferred_quote: quotes_settings.inline_quotes, + }, + *range, + )); + continue; + } + let mut diagnostic = Diagnostic::new( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes,