diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D300.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D300.py new file mode 100644 index 0000000000000..eb9b4c57307da --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D300.py @@ -0,0 +1,10 @@ +def with_backslash(): + """Sum\\mary.""" + + +def ends_in_quote(): + 'Sum\\mary."' + + +def contains_quote(): + 'Sum"\\mary.' diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index e3e51c00c6387..59931647d9e9b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -87,6 +87,7 @@ mod tests { #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))] #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))] #[test_case(Rule::TripleSingleQuotes, Path::new("D.py"))] + #[test_case(Rule::TripleSingleQuotes, Path::new("D300.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs index c8b7d6208a80c..9ec1dd66d9ebc 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_codegen::Quote; use ruff_text_size::Ranged; @@ -37,6 +37,8 @@ pub struct TripleSingleQuotes { } impl Violation for TripleSingleQuotes { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let TripleSingleQuotes { expected_quote } = self; @@ -45,12 +47,25 @@ impl Violation for TripleSingleQuotes { Quote::Single => format!(r#"Use triple single quotes `'''`"#), } } + + fn fix_title(&self) -> Option { + let TripleSingleQuotes { expected_quote } = self; + Some(match expected_quote { + Quote::Double => format!("Convert to triple double quotes"), + Quote::Single => format!("Convert to triple single quotes"), + }) + } } /// D300 pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { let leading_quote = docstring.leading_quote(); + let prefixes = docstring + .leading_quote() + .trim_end_matches(|c| c == '\'' || c == '"') + .to_owned(); + let expected_quote = if docstring.body().contains("\"\"\"") { Quote::Single } else { @@ -60,18 +75,34 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { match expected_quote { Quote::Single => { if !leading_quote.ends_with("'''") { - checker.diagnostics.push(Diagnostic::new( - TripleSingleQuotes { expected_quote }, - docstring.range(), - )); + let mut diagnostic = + Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range()); + + let body = docstring.body().as_str(); + if !body.ends_with('\'') { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + format!("{prefixes}'''{body}'''"), + docstring.range(), + ))); + } + + checker.diagnostics.push(diagnostic); } } Quote::Double => { if !leading_quote.ends_with("\"\"\"") { - checker.diagnostics.push(Diagnostic::new( - TripleSingleQuotes { expected_quote }, - docstring.range(), - )); + let mut diagnostic = + Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range()); + + let body = docstring.body().as_str(); + if !body.ends_with('"') { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + format!("{prefixes}\"\"\"{body}\"\"\""), + docstring.range(), + ))); + } + + checker.diagnostics.push(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap index dbf703c165317..419c4e237ae64 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D.py.snap @@ -1,47 +1,102 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -D.py:307:5: D300 Use triple double quotes `"""` +D.py:307:5: D300 [*] Use triple double quotes `"""` | 305 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)') 306 | def triple_single_quotes_raw(): 307 | r'''Summary.''' | ^^^^^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes -D.py:312:5: D300 Use triple double quotes `"""` +ℹ Fix +304 304 | +305 305 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)') +306 306 | def triple_single_quotes_raw(): +307 |- r'''Summary.''' + 307 |+ r"""Summary.""" +308 308 | +309 309 | +310 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)') + +D.py:312:5: D300 [*] Use triple double quotes `"""` | 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)') 311 | def triple_single_quotes_raw_uppercase(): 312 | R'''Summary.''' | ^^^^^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +309 309 | +310 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)') +311 311 | def triple_single_quotes_raw_uppercase(): +312 |- R'''Summary.''' + 312 |+ R"""Summary.""" +313 313 | +314 314 | +315 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') -D.py:317:5: D300 Use triple double quotes `"""` +D.py:317:5: D300 [*] Use triple double quotes `"""` | 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') 316 | def single_quotes_raw(): 317 | r'Summary.' | ^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +314 314 | +315 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') +316 316 | def single_quotes_raw(): +317 |- r'Summary.' + 317 |+ r"""Summary.""" +318 318 | +319 319 | +320 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') -D.py:322:5: D300 Use triple double quotes `"""` +D.py:322:5: D300 [*] Use triple double quotes `"""` | 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') 321 | def single_quotes_raw_uppercase(): 322 | R'Summary.' | ^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes -D.py:328:5: D300 Use triple double quotes `"""` +ℹ Fix +319 319 | +320 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') +321 321 | def single_quotes_raw_uppercase(): +322 |- R'Summary.' + 322 |+ R"""Summary.""" +323 323 | +324 324 | +325 325 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') + +D.py:328:5: D300 [*] Use triple double quotes `"""` | 326 | @expect('D301: Use r""" if any backslashes in a docstring') 327 | def single_quotes_raw_uppercase_backslash(): 328 | R'Sum\mary.' | ^^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +325 325 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') +326 326 | @expect('D301: Use r""" if any backslashes in a docstring') +327 327 | def single_quotes_raw_uppercase_backslash(): +328 |- R'Sum\mary.' + 328 |+ R"""Sum\mary.""" +329 329 | +330 330 | +331 331 | @expect('D301: Use r""" if any backslashes in a docstring') -D.py:645:5: D300 Use triple double quotes `"""` +D.py:645:5: D300 [*] Use triple double quotes `"""` | 644 | def single_line_docstring_with_an_escaped_backslash(): 645 | "\ @@ -51,8 +106,21 @@ D.py:645:5: D300 Use triple double quotes `"""` 647 | 648 | class StatementOnSameLineAsDocstring: | + = help: Convert to triple double quotes -D.py:649:5: D300 Use triple double quotes `"""` +ℹ Fix +642 642 | +643 643 | +644 644 | def single_line_docstring_with_an_escaped_backslash(): +645 |- "\ +646 |- " + 645 |+ """\ + 646 |+ """ +647 647 | +648 648 | class StatementOnSameLineAsDocstring: +649 649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1 + +D.py:649:5: D300 [*] Use triple double quotes `"""` | 648 | class StatementOnSameLineAsDocstring: 649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1 @@ -60,15 +128,37 @@ D.py:649:5: D300 Use triple double quotes `"""` 650 | def sort_services(self): 651 | pass | + = help: Convert to triple double quotes + +ℹ Fix +646 646 | " +647 647 | +648 648 | class StatementOnSameLineAsDocstring: +649 |- "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1 + 649 |+ """After this docstring there's another statement on the same line separated by a semicolon.""" ; priorities=1 +650 650 | def sort_services(self): +651 651 | pass +652 652 | -D.py:654:5: D300 Use triple double quotes `"""` +D.py:654:5: D300 [*] Use triple double quotes `"""` | 653 | class StatementOnSameLineAsDocstring: 654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +651 651 | pass +652 652 | +653 653 | class StatementOnSameLineAsDocstring: +654 |- "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1 + 654 |+ """After this docstring there's another statement on the same line separated by a semicolon."""; priorities=1 +655 655 | +656 656 | +657 657 | class CommentAfterDocstring: -D.py:658:5: D300 Use triple double quotes `"""` +D.py:658:5: D300 [*] Use triple double quotes `"""` | 657 | class CommentAfterDocstring: 658 | "After this docstring there's a comment." # priorities=1 @@ -76,8 +166,19 @@ D.py:658:5: D300 Use triple double quotes `"""` 659 | def sort_services(self): 660 | pass | + = help: Convert to triple double quotes -D.py:664:5: D300 Use triple double quotes `"""` +ℹ Fix +655 655 | +656 656 | +657 657 | class CommentAfterDocstring: +658 |- "After this docstring there's a comment." # priorities=1 + 658 |+ """After this docstring there's a comment.""" # priorities=1 +659 659 | def sort_services(self): +660 660 | pass +661 661 | + +D.py:664:5: D300 [*] Use triple double quotes `"""` | 663 | def newline_after_closing_quote(self): 664 | "We enforce a newline after the closing quote for a multi-line docstring \ @@ -85,5 +186,15 @@ D.py:664:5: D300 Use triple double quotes `"""` 665 | | but continuations shouldn't be considered multi-line" | |_________________________________________________________^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +661 661 | +662 662 | +663 663 | def newline_after_closing_quote(self): +664 |- "We enforce a newline after the closing quote for a multi-line docstring \ +665 |- but continuations shouldn't be considered multi-line" + 664 |+ """We enforce a newline after the closing quote for a multi-line docstring \ + 665 |+ but continuations shouldn't be considered multi-line""" diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap new file mode 100644 index 0000000000000..3b1b637e90a4c --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D300_D300.py.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D300.py:6:5: D300 Use triple double quotes `"""` + | +5 | def ends_in_quote(): +6 | 'Sum\\mary."' + | ^^^^^^^^^^^^^ D300 + | + = help: Convert to triple double quotes + +D300.py:10:5: D300 [*] Use triple double quotes `"""` + | + 9 | def contains_quote(): +10 | 'Sum"\\mary.' + | ^^^^^^^^^^^^^ D300 + | + = help: Convert to triple double quotes + +ℹ Fix +7 7 | +8 8 | +9 9 | def contains_quote(): +10 |- 'Sum"\\mary.' + 10 |+ """Sum"\\mary.""" + + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap index 1c7757d4b4b3e..53d779dfaf713 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__bom.snap @@ -1,10 +1,15 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -bom.py:1:1: D300 Use triple double quotes `"""` +bom.py:1:1: D300 [*] Use triple double quotes `"""` | 1 | ''' SAM macro definitions ''' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300 | + = help: Convert to triple double quotes + +ℹ Fix +1 |-''' SAM macro definitions ''' + 1 |+""" SAM macro definitions """