Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix D204 when there's a semicolon after a docstring #7174

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/ruff/resources/test/fixtures/pydocstyle/D.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,3 +644,17 @@ def same_line(): """This is a docstring on the same line"""
def single_line_docstring_with_an_escaped_backslash():
"\
"

class StatementOnSameLineAsDocstring:
"After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
def sort_services(self):
pass

class StatementOnSameLineAsDocstring:
"After this docstring there's another statement on the same line separated by a semicolon."; priorities=1


class CommentAfterDocstring:
"After this docstring there's a comment." # priorities=1
def sort_services(self):
pass
58 changes: 47 additions & 11 deletions crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_trivia::PythonWhitespace;
use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines};
use ruff_python_trivia::{indentation_at_offset, PythonWhitespace};
use ruff_source_file::{Line, UniversalNewlineIterator};
use ruff_text_size::Ranged;
use ruff_text_size::{TextLen, TextRange};

Expand Down Expand Up @@ -216,25 +216,61 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
}

if checker.enabled(Rule::OneBlankLineAfterClass) {
let after_range = TextRange::new(docstring.end(), class.end());

let after = checker.locator().slice(after_range);
let class_after_docstring_range = TextRange::new(docstring.end(), class.end());
let class_after_docstring = checker.locator().slice(class_after_docstring_range);
let mut lines = UniversalNewlineIterator::with_offset(
class_after_docstring,
class_after_docstring_range.start(),
);

let all_blank_after = after.universal_newlines().skip(1).all(|line| {
// If the class is empty except for comments, we don't need to insert a newline between
// docstring and no content
let all_blank_after = lines.clone().all(|line| {
line.trim_whitespace().is_empty() || line.trim_whitespace_start().starts_with('#')
});
if all_blank_after {
return;
}

let mut lines = UniversalNewlineIterator::with_offset(after, after_range.start());
let first_line = lines.next();
let mut replacement_start = first_line.as_ref().map(Line::start).unwrap_or_default();

// Edge case: There is trailing end-of-line content after the docstring, either a statement
// separated by a semicolon or a comment.
if let Some(first_line) = &first_line {
konstin marked this conversation as resolved.
Show resolved Hide resolved
let trailing = first_line.as_str().trim_whitespace_start();
if let Some(next_statement) = trailing.strip_prefix(';') {
let indentation = indentation_at_offset(docstring.start(), checker.locator())
.expect("Own line docstring must have indentation");
let mut diagnostic = Diagnostic::new(OneBlankLineAfterClass, docstring.range());
if checker.patch(diagnostic.kind.rule()) {
let line_ending = checker.stylist().line_ending().as_str();
// We have to trim the whitespace twice, once before the semicolon above and
// once after the semicolon here, or we get invalid indents:
// ```rust
// class Priority:
// """Has priorities""" ; priorities=1
// ```
let next_statement = next_statement.trim_whitespace_start();
diagnostic.set_fix(Fix::automatic(Edit::replacement(
line_ending.to_string() + line_ending + indentation + next_statement,
replacement_start,
first_line.end(),
)));
}
checker.diagnostics.push(diagnostic);
return;
} else if trailing.starts_with('#') {
// Keep the end-of-line comment, start counting empty lines after it
replacement_start = first_line.end();
}
}

let first_line_start = lines.next().map(|l| l.start()).unwrap_or_default();
let mut blank_lines_after = 0usize;
let mut blank_lines_end = docstring.end();
let mut blank_lines_end = first_line.as_ref().map_or(docstring.end(), Line::end);

for line in lines {
if line.trim().is_empty() {
if line.trim_whitespace().is_empty() {
blank_lines_end = line.end();
blank_lines_after += 1;
} else {
Expand All @@ -248,7 +284,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
// Insert a blank line before the class (replacing any existing lines).
diagnostic.set_fix(Fix::automatic(Edit::replacement(
checker.stylist().line_ending().to_string(),
first_line_start,
replacement_start,
blank_lines_end,
)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ D.py:68:9: D102 Missing docstring in public method
69 | pass
|

D.py:650:9: D102 Missing docstring in public method
|
648 | class StatementOnSameLineAsDocstring:
649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
650 | def sort_services(self):
| ^^^^^^^^^^^^^ D102
651 | pass
|

D.py:659:9: D102 Missing docstring in public method
|
657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1
659 | def sort_services(self):
| ^^^^^^^^^^^^^ D102
660 | pass
|


Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ D.py:645:5: D200 One-line docstring should fit on one line
| _____^
646 | | "
| |_____^ D200
647 |
648 | class StatementOnSameLineAsDocstring:
|
= help: Reformat to one line

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,59 @@ D.py:526:5: D203 [*] 1 blank line required before class docstring
527 528 |
528 529 | Parameters

D.py:649:5: D203 [*] 1 blank line required before class docstring
|
648 | class StatementOnSameLineAsDocstring:
649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D203
650 | def sort_services(self):
651 | pass
|
= help: Insert 1 blank line before class docstring

ℹ Fix
646 646 | "
647 647 |
648 648 | class StatementOnSameLineAsDocstring:
649 |+
649 650 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
650 651 | def sort_services(self):
651 652 | pass

D.py:654:5: D203 [*] 1 blank line required before class docstring
|
653 | class StatementOnSameLineAsDocstring:
654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D203
|
= help: Insert 1 blank line before class docstring

ℹ Fix
651 651 | pass
652 652 |
653 653 | class StatementOnSameLineAsDocstring:
654 |+
654 655 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
655 656 |
656 657 |

D.py:658:5: D203 [*] 1 blank line required before class docstring
|
657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D203
659 | def sort_services(self):
660 | pass
|
= help: Insert 1 blank line before class docstring

ℹ Fix
655 655 |
656 656 |
657 657 | class CommentAfterDocstring:
658 |+
658 659 | "After this docstring there's a comment." # priorities=1
659 660 | def sort_services(self):
660 661 | pass


Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,64 @@ D.py:192:5: D204 [*] 1 blank line required after class docstring
194 195 |
195 196 |

D.py:649:5: D204 [*] 1 blank line required after class docstring
|
648 | class StatementOnSameLineAsDocstring:
649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D204
650 | def sort_services(self):
651 | pass
|
= help: Insert 1 blank line after class docstring

ℹ 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."
650 |+
651 |+ priorities=1
650 652 | def sort_services(self):
651 653 | pass
652 654 |

D.py:654:5: D204 [*] 1 blank line required after class docstring
|
653 | class StatementOnSameLineAsDocstring:
654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D204
|
= help: Insert 1 blank line after class docstring

ℹ 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."
655 |+
656 |+ priorities=1
655 657 |
656 658 |
657 659 | class CommentAfterDocstring:

D.py:658:5: D204 [*] 1 blank line required after class docstring
|
657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D204
659 | def sort_services(self):
660 | pass
|
= help: Insert 1 blank line after class docstring

ℹ Fix
656 656 |
657 657 | class CommentAfterDocstring:
658 658 | "After this docstring there's a comment." # priorities=1
659 |+
659 660 | def sort_services(self):
660 661 | pass


Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ D.py:645:5: D300 Use triple double quotes `"""`
| _____^
646 | | "
| |_____^ D300
647 |
648 | class StatementOnSameLineAsDocstring:
|

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
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
650 | def sort_services(self):
651 | pass
|

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
|

D.py:658:5: D300 Use triple double quotes `"""`
|
657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
659 | def sort_services(self):
660 | pass
|


1 change: 1 addition & 0 deletions crates/ruff_source_file/src/newlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl UniversalNewlines for str {
/// assert_eq!(lines.next_back(), Some(Line::new("\r\n", TextSize::from(8))));
/// assert_eq!(lines.next(), None);
/// ```
#[derive(Clone)]
pub struct UniversalNewlineIterator<'a> {
text: &'a str,
offset: TextSize,
Expand Down