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_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_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/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/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 07ede87903f59..7ecfbd2d375fb 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( @@ -127,6 +129,10 @@ 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"))] + #[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( @@ -161,6 +167,10 @@ 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"))] + #[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/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index fdad2d1cc12a4..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 @@ -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()), } } } @@ -188,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 => '\'', } } @@ -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,38 @@ impl<'a> From<&'a str> for Trivia<'a> { } } +/// 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 { + locator + .after(range.end()) + .starts_with(good_docstring(quote)) +} + /// 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.flake8_quotes.docstring_quotes) + { + // 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, + )); + } if trivia .raw_text @@ -253,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); @@ -344,6 +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) + { + // 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, + )); + 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, 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..ead01a887e0ca --- /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,56 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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: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'''): +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: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 +8 | +9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass + | ^^ 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 new file mode 100644 index 0000000000000..67203cf8ff337 --- /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,106 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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: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'''): +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: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 +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 + +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 new file mode 100644 index 0000000000000..b12ef7e5b6e9c --- /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,36 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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 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(): +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..a0f9cc158c40a --- /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,50 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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: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(): +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_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..96ccdbd7f484f --- /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,29 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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..e02c3c17c1e3e --- /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,54 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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 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..df92925a9bd38 --- /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,11 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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 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_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..31efd169aff73 --- /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,18 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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 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..3c5b35cd44202 --- /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,49 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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..031164bad78ba --- /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,23 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +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'