mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 15:15:33 +00:00
Avoid removing newlines between docstring headers and rST blocks (#11609)
Given: ```python def func(): """ Example: .. code-block:: python import foo """ ``` Removing the newline after the `Example:` header breaks Sphinx rendering. See: https://github.com/astral-sh/ruff/issues/11577
This commit is contained in:
parent
b0a751012e
commit
3aa7e35a4c
4 changed files with 254 additions and 26 deletions
67
crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx.py
vendored
Normal file
67
crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx.py
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
|
@ -46,6 +46,7 @@ mod tests {
|
|||
#[test_case(Rule::BlankLineBeforeClass, Path::new("D.py"))]
|
||||
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"))]
|
||||
#[test_case(Rule::BlankLinesBetweenHeaderAndContent, Path::new("sections.py"))]
|
||||
#[test_case(Rule::BlankLinesBetweenHeaderAndContent, Path::new("sphinx.py"))]
|
||||
#[test_case(Rule::OverIndentation, Path::new("D.py"))]
|
||||
#[test_case(Rule::OverIndentation, Path::new("D208.py"))]
|
||||
#[test_case(Rule::NoSignature, Path::new("D.py"))]
|
||||
|
|
|
@ -1393,14 +1393,14 @@ fn blanks_and_section_underline(
|
|||
docstring: &Docstring,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
let mut blank_lines_after_header = 0;
|
||||
let mut num_blank_lines_after_header = 0u32;
|
||||
let mut blank_lines_end = context.following_range().start();
|
||||
let mut following_lines = context.following_lines().peekable();
|
||||
|
||||
while let Some(line) = following_lines.peek() {
|
||||
if line.trim().is_empty() {
|
||||
blank_lines_end = line.full_end();
|
||||
blank_lines_after_header += 1;
|
||||
num_blank_lines_after_header += 1;
|
||||
following_lines.next();
|
||||
} else {
|
||||
break;
|
||||
|
@ -1409,7 +1409,7 @@ fn blanks_and_section_underline(
|
|||
|
||||
if let Some(non_blank_line) = following_lines.next() {
|
||||
if let Some(dashed_line) = find_underline(&non_blank_line, '-') {
|
||||
if blank_lines_after_header > 0 {
|
||||
if num_blank_lines_after_header > 0 {
|
||||
if checker.enabled(Rule::SectionUnderlineAfterName) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SectionUnderlineAfterName {
|
||||
|
@ -1475,10 +1475,12 @@ fn blanks_and_section_underline(
|
|||
|
||||
if let Some(line_after_dashes) = following_lines.next() {
|
||||
if line_after_dashes.trim().is_empty() {
|
||||
let mut num_blank_lines_dashes_end = 1u32;
|
||||
let mut blank_lines_after_dashes_end = line_after_dashes.full_end();
|
||||
while let Some(line) = following_lines.peek() {
|
||||
if line.trim().is_empty() {
|
||||
blank_lines_after_dashes_end = line.full_end();
|
||||
num_blank_lines_dashes_end += 1;
|
||||
following_lines.next();
|
||||
} else {
|
||||
break;
|
||||
|
@ -1495,6 +1497,44 @@ fn blanks_and_section_underline(
|
|||
));
|
||||
}
|
||||
} else if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) {
|
||||
// If the section is followed by exactly one line, and then a
|
||||
// reStructuredText directive, the blank lines should be preserved, as in:
|
||||
//
|
||||
// ```python
|
||||
// """
|
||||
// Example
|
||||
// -------
|
||||
//
|
||||
// .. code-block:: python
|
||||
//
|
||||
// import foo
|
||||
// """
|
||||
// ```
|
||||
//
|
||||
// Otherwise, documentation generators will not recognize the directive.
|
||||
let is_sphinx = checker
|
||||
.locator()
|
||||
.line(blank_lines_after_dashes_end)
|
||||
.trim_start()
|
||||
.starts_with(".. ");
|
||||
|
||||
if is_sphinx {
|
||||
if num_blank_lines_dashes_end > 1 {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesBetweenHeaderAndContent {
|
||||
name: context.section_name().to_string(),
|
||||
},
|
||||
context.section_name_range(),
|
||||
);
|
||||
// Preserve a single blank line between the header and content.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||
checker.stylist().line_ending().to_string(),
|
||||
line_after_dashes.start(),
|
||||
blank_lines_after_dashes_end,
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesBetweenHeaderAndContent {
|
||||
name: context.section_name().to_string(),
|
||||
|
@ -1509,6 +1549,7 @@ fn blanks_and_section_underline(
|
|||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if checker.enabled(Rule::EmptyDocstringSection) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
|
@ -1560,21 +1601,62 @@ fn blanks_and_section_underline(
|
|||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if blank_lines_after_header > 0 {
|
||||
if num_blank_lines_after_header > 0 {
|
||||
if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) {
|
||||
// If the section is followed by exactly one line, and then a
|
||||
// reStructuredText directive, the blank lines should be preserved, as in:
|
||||
//
|
||||
// ```python
|
||||
// """
|
||||
// Example:
|
||||
//
|
||||
// .. code-block:: python
|
||||
//
|
||||
// import foo
|
||||
// """
|
||||
// ```
|
||||
//
|
||||
// Otherwise, documentation generators will not recognize the directive.
|
||||
let is_sphinx = checker
|
||||
.locator()
|
||||
.line(blank_lines_end)
|
||||
.trim_start()
|
||||
.starts_with(".. ");
|
||||
|
||||
if is_sphinx {
|
||||
if num_blank_lines_after_header > 1 {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesBetweenHeaderAndContent {
|
||||
name: context.section_name().to_string(),
|
||||
},
|
||||
context.section_name_range(),
|
||||
);
|
||||
let range = TextRange::new(context.following_range().start(), blank_lines_end);
|
||||
|
||||
// Preserve a single blank line between the header and content.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||
checker.stylist().line_ending().to_string(),
|
||||
context.following_range().start(),
|
||||
blank_lines_end,
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BlankLinesBetweenHeaderAndContent {
|
||||
name: context.section_name().to_string(),
|
||||
},
|
||||
context.section_name_range(),
|
||||
);
|
||||
|
||||
let range =
|
||||
TextRange::new(context.following_range().start(), blank_lines_end);
|
||||
// Delete any blank lines between the header and content.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nothing but blank lines after the section header.
|
||||
if checker.enabled(Rule::DashedUnderlineAfterSection) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
sphinx.py:13:5: D412 [*] No blank lines allowed between a section header and its content ("Example")
|
||||
|
|
||||
11 | def func():
|
||||
12 | """
|
||||
13 | Example:
|
||||
| ^^^^^^^ D412
|
||||
|
|
||||
= help: Remove blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | """
|
||||
13 13 | Example:
|
||||
14 14 |
|
||||
15 |-
|
||||
16 15 | .. code-block:: python
|
||||
17 16 |
|
||||
18 17 | import foo
|
||||
|
||||
sphinx.py:24:5: D412 [*] No blank lines allowed between a section header and its content ("Example")
|
||||
|
|
||||
22 | def func():
|
||||
23 | """
|
||||
24 | Example:
|
||||
| ^^^^^^^ D412
|
||||
|
|
||||
= help: Remove blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
23 23 | """
|
||||
24 24 | Example:
|
||||
25 25 |
|
||||
26 |-
|
||||
27 |-
|
||||
28 26 | .. code-block:: python
|
||||
29 27 |
|
||||
30 28 | import foo
|
||||
|
||||
sphinx.py:47:5: D412 [*] No blank lines allowed between a section header and its content ("Example")
|
||||
|
|
||||
45 | def func():
|
||||
46 | """
|
||||
47 | Example
|
||||
| ^^^^^^^ D412
|
||||
48 | -------
|
||||
|
|
||||
= help: Remove blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
47 47 | Example
|
||||
48 48 | -------
|
||||
49 49 |
|
||||
50 |-
|
||||
51 50 | .. code-block:: python
|
||||
52 51 |
|
||||
53 52 | import foo
|
||||
|
||||
sphinx.py:59:5: D412 [*] No blank lines allowed between a section header and its content ("Example")
|
||||
|
|
||||
57 | def func():
|
||||
58 | """
|
||||
59 | Example
|
||||
| ^^^^^^^ D412
|
||||
60 | -------
|
||||
|
|
||||
= help: Remove blank line(s)
|
||||
|
||||
ℹ Safe fix
|
||||
59 59 | Example
|
||||
60 60 | -------
|
||||
61 61 |
|
||||
62 |-
|
||||
63 |-
|
||||
64 62 | .. code-block:: python
|
||||
65 63 |
|
||||
66 64 | import foo
|
Loading…
Add table
Add a link
Reference in a new issue