Skip to content

Commit a6abd65

Browse files
danparizherntBre
andauthored
[pydoclint] Fix false positive when Sphinx directives follow Raises section (DOC502) (#20535)
## Summary Fixes #18959 --------- Co-authored-by: Brent Westbrook <[email protected]>
1 parent 3d4b055 commit a6abd65

File tree

4 files changed

+111
-6
lines changed

4 files changed

+111
-6
lines changed

crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,37 @@ def calculate_speed(distance: float, time: float) -> float:
8383
raise
8484

8585

86+
# DOC502 regression for Sphinx directive after Raises (issue #18959)
87+
def foo():
88+
"""First line.
89+
90+
Raises:
91+
ValueError:
92+
some text
93+
94+
.. versionadded:: 0.7.0
95+
The ``init_kwargs`` argument.
96+
"""
97+
raise ValueError
98+
99+
100+
# DOC502 regression for following section with colons
101+
def example_with_following_section():
102+
"""Summary.
103+
104+
Returns:
105+
str: The resulting expression.
106+
107+
Raises:
108+
ValueError: If the unit is not valid.
109+
110+
Relation to `time_range_lookup`:
111+
- Handles the "start of" modifier.
112+
- Example: "start of month" → `DATETRUNC()`.
113+
"""
114+
raise ValueError
115+
116+
86117
# This should NOT trigger DOC502 because OSError is explicitly re-raised
87118
def f():
88119
"""Do nothing.

crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,33 @@ def calculate_speed(distance: float, time: float) -> float:
117117
except TypeError:
118118
print("Not a number? Shame on you!")
119119
raise
120+
121+
122+
# DOC502 regression for Sphinx directive after Raises (issue #18959)
123+
def foo():
124+
"""First line.
125+
126+
Raises
127+
------
128+
ValueError
129+
some text
130+
131+
.. versionadded:: 0.7.0
132+
The ``init_kwargs`` argument.
133+
"""
134+
raise ValueError
135+
136+
# Make sure we don't bail out on a Sphinx directive in the description of one
137+
# of the exceptions
138+
def foo():
139+
"""First line.
140+
141+
Raises
142+
------
143+
ValueError
144+
some text
145+
.. math:: e^{xception}
146+
ZeroDivisionError
147+
Will not be raised, DOC502
148+
"""
149+
raise ValueError

crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -722,12 +722,30 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
722722
/// ```
723723
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
724724
let mut entries: Vec<QualifiedName> = Vec::new();
725-
for potential in content.lines() {
726-
let Some(colon_idx) = potential.find(':') else {
727-
continue;
728-
};
729-
let entry = potential[..colon_idx].trim();
730-
entries.push(QualifiedName::user_defined(entry));
725+
let mut lines = content.lines().peekable();
726+
let Some(first) = lines.peek() else {
727+
return entries;
728+
};
729+
let indentation = &first[..first.len() - first.trim_start().len()];
730+
for potential in lines {
731+
if let Some(entry) = potential.strip_prefix(indentation) {
732+
if let Some(first_char) = entry.chars().next() {
733+
if !first_char.is_whitespace() {
734+
if let Some(colon_idx) = entry.find(':') {
735+
let entry = entry[..colon_idx].trim();
736+
if !entry.is_empty() {
737+
entries.push(QualifiedName::user_defined(entry));
738+
}
739+
}
740+
}
741+
}
742+
} else {
743+
// If we can't strip the expected indentation, check if this is a dedented line
744+
// (not blank) - if so, break early as we've reached the end of this section
745+
if !potential.trim().is_empty() {
746+
break;
747+
}
748+
}
731749
}
732750
entries
733751
}
@@ -751,6 +769,12 @@ fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
751769
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
752770
for potential in lines {
753771
if let Some(entry) = potential.strip_prefix(indentation) {
772+
// Check for Sphinx directives (lines starting with ..) - these indicate the end of the
773+
// section. In numpy-style, exceptions are dedented to the same level as sphinx
774+
// directives.
775+
if entry.starts_with("..") {
776+
break;
777+
}
754778
if let Some(first_char) = entry.chars().next() {
755779
if !first_char.is_whitespace() {
756780
entries.push(QualifiedName::user_defined(entry.trim_end()));

crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,23 @@ DOC502 Raised exception is not explicitly raised: `DivisionByZero`
9595
82 | return distance / time
9696
|
9797
help: Remove `DivisionByZero` from the docstring
98+
99+
DOC502 Raised exception is not explicitly raised: `ZeroDivisionError`
100+
--> DOC502_numpy.py:139:5
101+
|
102+
137 | # of the exceptions
103+
138 | def foo():
104+
139 | / """First line.
105+
140 | |
106+
141 | | Raises
107+
142 | | ------
108+
143 | | ValueError
109+
144 | | some text
110+
145 | | .. math:: e^{xception}
111+
146 | | ZeroDivisionError
112+
147 | | Will not be raised, DOC502
113+
148 | | """
114+
| |_______^
115+
149 | raise ValueError
116+
|
117+
help: Remove `ZeroDivisionError` from the docstring

0 commit comments

Comments
 (0)