diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py new file mode 100644 index 0000000000000..1d04024f8c298 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py @@ -0,0 +1,59 @@ +"""Do something. + +Args: + x: the value + with a hanging indent + +Returns: + the value +""" + + +def func(): + """Do something. + + Args: + x: the value + with a hanging indent + + Returns: + the value + """ + + +def func(): + """Do something. + + Args: + x: the value + with a hanging indent + + Returns: + the value + + """ + + +def func(): + """Do something. + + Args: + x: the value + with a hanging indent + + Returns: + the value + + + """ + + +def func(): + """Do something. + + Args: + x: the value + with a hanging indent + + Returns: + the value""" diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index 5c08ee88dba7b..b4212ec2f8739 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -20,6 +20,7 @@ mod tests { #[test_case(Rule::BlankLineAfterLastSection, Path::new("sections.py"))] #[test_case(Rule::NoBlankLineAfterSection, Path::new("sections.py"))] + #[test_case(Rule::BlankLineAfterLastSection, Path::new("D413.py"))] #[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"))] #[test_case(Rule::NoBlankLineBeforeSection, Path::new("sections.py"))] #[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"))] diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 393ce0deacf1b..250838e0dd333 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1632,9 +1632,13 @@ fn common_section( } let line_end = checker.stylist().line_ending().as_str(); - let last_line = context.following_lines().last(); - if last_line.map_or(true, |line| !line.trim().is_empty()) { - if let Some(next) = next { + + if let Some(next) = next { + if context + .following_lines() + .last() + .map_or(true, |line| !line.trim().is_empty()) + { if checker.enabled(Rule::NoBlankLineAfterSection) { let mut diagnostic = Diagnostic::new( NoBlankLineAfterSection { @@ -1649,7 +1653,16 @@ fn common_section( ))); checker.diagnostics.push(diagnostic); } - } else { + } + } else { + // The first blank line is the line containing the closing triple quotes, so we need at + // least two. + let num_blank_lines = context + .following_lines() + .rev() + .take_while(|line| line.trim().is_empty()) + .count(); + if num_blank_lines < 2 { if checker.enabled(Rule::BlankLineAfterLastSection) { let mut diagnostic = Diagnostic::new( BlankLineAfterLastSection { @@ -1659,7 +1672,11 @@ fn common_section( ); // Add a newline after the section. diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - format!("{}{}", line_end, docstring.indentation), + format!( + "{}{}", + line_end.repeat(2 - num_blank_lines), + docstring.indentation + ), context.end(), ))); checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap new file mode 100644 index 0000000000000..eda7d334cfaad --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap @@ -0,0 +1,80 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D413.py:1:1: D413 [*] Missing blank line after last section ("Returns") + | +1 | / """Do something. +2 | | +3 | | Args: +4 | | x: the value +5 | | with a hanging indent +6 | | +7 | | Returns: +8 | | the value +9 | | """ + | |___^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +6 6 | +7 7 | Returns: +8 8 | the value + 9 |+ + 10 |+ +9 11 | """ +10 12 | +11 13 | + +D413.py:13:5: D413 [*] Missing blank line after last section ("Returns") + | +12 | def func(): +13 | """Do something. + | _____^ +14 | | +15 | | Args: +16 | | x: the value +17 | | with a hanging indent +18 | | +19 | | Returns: +20 | | the value +21 | | """ + | |_______^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +18 18 | +19 19 | Returns: +20 20 | the value + 21 |+ +21 22 | """ +22 23 | +23 24 | + +D413.py:52:5: D413 [*] Missing blank line after last section ("Returns") + | +51 | def func(): +52 | """Do something. + | _____^ +53 | | +54 | | Args: +55 | | x: the value +56 | | with a hanging indent +57 | | +58 | | Returns: +59 | | the value""" + | |____________________^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +56 56 | with a hanging indent +57 57 | +58 58 | Returns: +59 |- the value""" + 59 |+ the value + 60 |+ + 61 |+ """ + + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index 55af210426092..f2bf6474708dc 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -19,9 +19,147 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns") 66 66 | 67 |- Returns""" 67 |+ Returns - 68 |+ """ -68 69 | -69 70 | -70 71 | @expect(_D213) + 68 |+ + 69 |+ """ +68 70 | +69 71 | +70 72 | @expect(_D213) + +sections.py:120:5: D413 [*] Missing blank line after last section ("Returns") + | +118 | @expect("D413: Missing blank line after last section ('Returns')") +119 | def no_blank_line_after_last_section(): # noqa: D416 +120 | """Toggle the gizmo. + | _____^ +121 | | +122 | | Returns +123 | | ------- +124 | | A value of some sort. +125 | | """ + | |_______^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +122 122 | Returns +123 123 | ------- +124 124 | A value of some sort. + 125 |+ +125 126 | """ +126 127 | +127 128 | + +sections.py:170:5: D413 [*] Missing blank line after last section ("Returns") + | +168 | @expect("D414: Section has no content ('Returns')") +169 | def section_underline_overindented_and_contentless(): # noqa: D416 +170 | """Toggle the gizmo. + | _____^ +171 | | +172 | | Returns +173 | | ------- +174 | | """ + | |_______^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +171 171 | +172 172 | Returns +173 173 | ------- + 174 |+ +174 175 | """ +175 176 | +176 177 | + +sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters") + | +518 | def replace_equals_with_dash(): +519 | """Equal length equals should be replaced with dashes. + | _____^ +520 | | +521 | | Parameters +522 | | ========== +523 | | """ + | |_______^ D413 + | + = help: Add blank line after "Parameters" + +ℹ Safe fix +520 520 | +521 521 | Parameters +522 522 | ========== + 523 |+ +523 524 | """ +524 525 | +525 526 | + +sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters") + | +526 | def replace_equals_with_dash2(): +527 | """Here, the length of equals is not the same. + | _____^ +528 | | +529 | | Parameters +530 | | =========== +531 | | """ + | |_______^ D413 + | + = help: Add blank line after "Parameters" + +ℹ Safe fix +528 528 | +529 529 | Parameters +530 530 | =========== + 531 |+ +531 532 | """ +532 533 | +533 534 | + +sections.py:548:5: D413 [*] Missing blank line after last section ("Args") + | +547 | def lowercase_sub_section_header(): +548 | """Below, `returns:` should _not_ be considered a section header. + | _____^ +549 | | +550 | | Args: +551 | | Here's a note. +552 | | +553 | | returns: +554 | | """ + | |_______^ D413 + | + = help: Add blank line after "Args" + +ℹ Safe fix +551 551 | Here's a note. +552 552 | +553 553 | returns: + 554 |+ +554 555 | """ +555 556 | +556 557 | + +sections.py:558:5: D413 [*] Missing blank line after last section ("Returns") + | +557 | def titlecase_sub_section_header(): +558 | """Below, `Returns:` should be considered a section header. + | _____^ +559 | | +560 | | Args: +561 | | Here's a note. +562 | | +563 | | Returns: +564 | | """ + | |_______^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +561 561 | Here's a note. +562 562 | +563 563 | Returns: + 564 |+ +564 565 | """