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::BlankLineBeforeClass, Path::new("D.py"))]
|
||||||
#[test_case(Rule::NoBlankLineBeforeFunction, 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("sections.py"))]
|
||||||
|
#[test_case(Rule::BlankLinesBetweenHeaderAndContent, Path::new("sphinx.py"))]
|
||||||
#[test_case(Rule::OverIndentation, Path::new("D.py"))]
|
#[test_case(Rule::OverIndentation, Path::new("D.py"))]
|
||||||
#[test_case(Rule::OverIndentation, Path::new("D208.py"))]
|
#[test_case(Rule::OverIndentation, Path::new("D208.py"))]
|
||||||
#[test_case(Rule::NoSignature, Path::new("D.py"))]
|
#[test_case(Rule::NoSignature, Path::new("D.py"))]
|
||||||
|
|
|
@ -1393,14 +1393,14 @@ fn blanks_and_section_underline(
|
||||||
docstring: &Docstring,
|
docstring: &Docstring,
|
||||||
context: &SectionContext,
|
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 blank_lines_end = context.following_range().start();
|
||||||
let mut following_lines = context.following_lines().peekable();
|
let mut following_lines = context.following_lines().peekable();
|
||||||
|
|
||||||
while let Some(line) = following_lines.peek() {
|
while let Some(line) = following_lines.peek() {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
blank_lines_end = line.full_end();
|
blank_lines_end = line.full_end();
|
||||||
blank_lines_after_header += 1;
|
num_blank_lines_after_header += 1;
|
||||||
following_lines.next();
|
following_lines.next();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -1409,7 +1409,7 @@ fn blanks_and_section_underline(
|
||||||
|
|
||||||
if let Some(non_blank_line) = following_lines.next() {
|
if let Some(non_blank_line) = following_lines.next() {
|
||||||
if let Some(dashed_line) = find_underline(&non_blank_line, '-') {
|
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) {
|
if checker.enabled(Rule::SectionUnderlineAfterName) {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
SectionUnderlineAfterName {
|
SectionUnderlineAfterName {
|
||||||
|
@ -1475,10 +1475,12 @@ fn blanks_and_section_underline(
|
||||||
|
|
||||||
if let Some(line_after_dashes) = following_lines.next() {
|
if let Some(line_after_dashes) = following_lines.next() {
|
||||||
if line_after_dashes.trim().is_empty() {
|
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();
|
let mut blank_lines_after_dashes_end = line_after_dashes.full_end();
|
||||||
while let Some(line) = following_lines.peek() {
|
while let Some(line) = following_lines.peek() {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
blank_lines_after_dashes_end = line.full_end();
|
blank_lines_after_dashes_end = line.full_end();
|
||||||
|
num_blank_lines_dashes_end += 1;
|
||||||
following_lines.next();
|
following_lines.next();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -1495,6 +1497,44 @@ fn blanks_and_section_underline(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) {
|
} 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(
|
let mut diagnostic = Diagnostic::new(
|
||||||
BlankLinesBetweenHeaderAndContent {
|
BlankLinesBetweenHeaderAndContent {
|
||||||
name: context.section_name().to_string(),
|
name: context.section_name().to_string(),
|
||||||
|
@ -1509,6 +1549,7 @@ fn blanks_and_section_underline(
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if checker.enabled(Rule::EmptyDocstringSection) {
|
if checker.enabled(Rule::EmptyDocstringSection) {
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
|
@ -1560,21 +1601,62 @@ fn blanks_and_section_underline(
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if blank_lines_after_header > 0 {
|
if num_blank_lines_after_header > 0 {
|
||||||
if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) {
|
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(
|
let mut diagnostic = Diagnostic::new(
|
||||||
BlankLinesBetweenHeaderAndContent {
|
BlankLinesBetweenHeaderAndContent {
|
||||||
name: context.section_name().to_string(),
|
name: context.section_name().to_string(),
|
||||||
},
|
},
|
||||||
context.section_name_range(),
|
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.
|
// Delete any blank lines between the header and content.
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
|
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Nothing but blank lines after the section header.
|
// Nothing but blank lines after the section header.
|
||||||
if checker.enabled(Rule::DashedUnderlineAfterSection) {
|
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