Add autofix for D300 (#7967)

## Summary

Add fix for `D300`

## Test Plan

`cargo test` and manually
This commit is contained in:
Steve C 2023-10-17 09:37:46 -04:00 committed by GitHub
parent dc6b4ad2b4
commit 8a529925b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 205 additions and 20 deletions

View file

@ -0,0 +1,10 @@
def with_backslash():
"""Sum\\mary."""
def ends_in_quote():
'Sum\\mary."'
def contains_quote():
'Sum"\\mary.'

View file

@ -87,6 +87,7 @@ mod tests {
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))] #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))]
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))] #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))]
#[test_case(Rule::TripleSingleQuotes, Path::new("D.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<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -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_macros::{derive_message_formats, violation};
use ruff_python_codegen::Quote; use ruff_python_codegen::Quote;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -37,6 +37,8 @@ pub struct TripleSingleQuotes {
} }
impl Violation for TripleSingleQuotes { impl Violation for TripleSingleQuotes {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let TripleSingleQuotes { expected_quote } = self; let TripleSingleQuotes { expected_quote } = self;
@ -45,12 +47,25 @@ impl Violation for TripleSingleQuotes {
Quote::Single => format!(r#"Use triple single quotes `'''`"#), Quote::Single => format!(r#"Use triple single quotes `'''`"#),
} }
} }
fn fix_title(&self) -> Option<String> {
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 /// D300
pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
let leading_quote = docstring.leading_quote(); 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("\"\"\"") { let expected_quote = if docstring.body().contains("\"\"\"") {
Quote::Single Quote::Single
} else { } else {
@ -60,18 +75,34 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
match expected_quote { match expected_quote {
Quote::Single => { Quote::Single => {
if !leading_quote.ends_with("'''") { if !leading_quote.ends_with("'''") {
checker.diagnostics.push(Diagnostic::new( let mut diagnostic =
TripleSingleQuotes { expected_quote }, 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(), docstring.range(),
)); )));
}
checker.diagnostics.push(diagnostic);
} }
} }
Quote::Double => { Quote::Double => {
if !leading_quote.ends_with("\"\"\"") { if !leading_quote.ends_with("\"\"\"") {
checker.diagnostics.push(Diagnostic::new( let mut diagnostic =
TripleSingleQuotes { expected_quote }, 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(), docstring.range(),
)); )));
}
checker.diagnostics.push(diagnostic);
} }
} }
} }

View file

@ -1,47 +1,102 @@
--- ---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs 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)') 305 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
306 | def triple_single_quotes_raw(): 306 | def triple_single_quotes_raw():
307 | r'''Summary.''' 307 | r'''Summary.'''
| ^^^^^^^^^^^^^^^ D300 | ^^^^^^^^^^^^^^^ 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)') 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
311 | def triple_single_quotes_raw_uppercase(): 311 | def triple_single_quotes_raw_uppercase():
312 | R'''Summary.''' 312 | R'''Summary.'''
| ^^^^^^^^^^^^^^^ D300 | ^^^^^^^^^^^^^^^ D300
| |
= help: Convert to triple double quotes
D.py:317:5: D300 Use 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 `"""`
| |
315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
316 | def single_quotes_raw(): 316 | def single_quotes_raw():
317 | r'Summary.' 317 | r'Summary.'
| ^^^^^^^^^^^ D300 | ^^^^^^^^^^^ D300
| |
= help: Convert to triple double quotes
D.py:322:5: D300 Use 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 `"""`
| |
320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)') 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
321 | def single_quotes_raw_uppercase(): 321 | def single_quotes_raw_uppercase():
322 | R'Summary.' 322 | R'Summary.'
| ^^^^^^^^^^^ D300 | ^^^^^^^^^^^ 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') 326 | @expect('D301: Use r""" if any backslashes in a docstring')
327 | def single_quotes_raw_uppercase_backslash(): 327 | def single_quotes_raw_uppercase_backslash():
328 | R'Sum\mary.' 328 | R'Sum\mary.'
| ^^^^^^^^^^^^ D300 | ^^^^^^^^^^^^ D300
| |
= help: Convert to triple double quotes
D.py:645:5: D300 Use 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 `"""`
| |
644 | def single_line_docstring_with_an_escaped_backslash(): 644 | def single_line_docstring_with_an_escaped_backslash():
645 | "\ 645 | "\
@ -51,8 +106,21 @@ D.py:645:5: D300 Use triple double quotes `"""`
647 | 647 |
648 | class StatementOnSameLineAsDocstring: 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: 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
@ -60,15 +128,37 @@ D.py:649:5: D300 Use triple double quotes `"""`
650 | def sort_services(self): 650 | def sort_services(self):
651 | pass 651 | pass
| |
= help: Convert to triple double quotes
D.py:654:5: D300 Use 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 `"""`
| |
653 | class StatementOnSameLineAsDocstring: 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
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
| |
= help: Convert to triple double quotes
D.py:658:5: D300 Use 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 `"""`
| |
657 | class CommentAfterDocstring: 657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1 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): 659 | def sort_services(self):
660 | pass 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): 663 | def newline_after_closing_quote(self):
664 | "We enforce a newline after the closing quote for a multi-line docstring \ 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" 665 | | but continuations shouldn't be considered multi-line"
| |_________________________________________________________^ D300 | |_________________________________________________________^ 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"""

View file

@ -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."""

View file

@ -1,10 +1,15 @@
--- ---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs 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 ''' 1 | ''' SAM macro definitions '''
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
| |
= help: Convert to triple double quotes
Fix
1 |-''' SAM macro definitions '''
1 |+""" SAM macro definitions """