diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py index 9709d9ff53637a..916b11c6345ae6 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_google.py @@ -83,6 +83,37 @@ def calculate_speed(distance: float, time: float) -> float: raise +# DOC502 regression for Sphinx directive after Raises (issue #18959) +def foo(): + """First line. + + Raises: + ValueError: + some text + + .. versionadded:: 0.7.0 + The ``init_kwargs`` argument. + """ + raise ValueError + + +# DOC502 regression for following section with colons +def example_with_following_section(): + """Summary. + + Returns: + str: The resulting expression. + + Raises: + ValueError: If the unit is not valid. + + Relation to `time_range_lookup`: + - Handles the "start of" modifier. + - Example: "start of month" → `DATETRUNC()`. + """ + raise ValueError + + # This should NOT trigger DOC502 because OSError is explicitly re-raised def f(): """Do nothing. diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py index 5e8bf5f36ef81a..c32814597d6941 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_numpy.py @@ -117,3 +117,33 @@ def calculate_speed(distance: float, time: float) -> float: except TypeError: print("Not a number? Shame on you!") raise + + +# DOC502 regression for Sphinx directive after Raises (issue #18959) +def foo(): + """First line. + + Raises + ------ + ValueError + some text + + .. versionadded:: 0.7.0 + The ``init_kwargs`` argument. + """ + raise ValueError + +# Make sure we don't bail out on a Sphinx directive in the description of one +# of the exceptions +def foo(): + """First line. + + Raises + ------ + ValueError + some text + .. math:: e^{xception} + ZeroDivisionError + Will not be raised, DOC502 + """ + raise ValueError diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index dd88250952fa4e..1a40718ab5c778 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -710,12 +710,30 @@ fn parse_raises(content: &str, style: Option) -> Vec Vec> { let mut entries: Vec = Vec::new(); - for potential in content.lines() { - let Some(colon_idx) = potential.find(':') else { - continue; - }; - let entry = potential[..colon_idx].trim(); - entries.push(QualifiedName::user_defined(entry)); + let mut lines = content.lines().peekable(); + let Some(first) = lines.peek() else { + return entries; + }; + let indentation = &first[..first.len() - first.trim_start().len()]; + for potential in lines { + if let Some(entry) = potential.strip_prefix(indentation) { + if let Some(first_char) = entry.chars().next() { + if !first_char.is_whitespace() { + if let Some(colon_idx) = entry.find(':') { + let entry = entry[..colon_idx].trim(); + if !entry.is_empty() { + entries.push(QualifiedName::user_defined(entry)); + } + } + } + } + } else { + // If we can't strip the expected indentation, check if this is a dedented line + // (not blank) - if so, break early as we've reached the end of this section + if !potential.trim().is_empty() { + break; + } + } } entries } @@ -739,6 +757,12 @@ fn parse_raises_numpy(content: &str) -> Vec> { let indentation = &dashes[..dashes.len() - dashes.trim_start().len()]; for potential in lines { if let Some(entry) = potential.strip_prefix(indentation) { + // Check for Sphinx directives (lines starting with ..) - these indicate the end of the + // section. In numpy-style, exceptions are dedented to the same level as sphinx + // directives. + if entry.starts_with("..") { + break; + } if let Some(first_char) = entry.chars().next() { if !first_char.is_whitespace() { entries.push(QualifiedName::user_defined(entry.trim_end())); diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap index fd28bded5d9435..2bb6d04b0bc3ae 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_numpy.py.snap @@ -95,3 +95,23 @@ DOC502 Raised exception is not explicitly raised: `DivisionByZero` 82 | return distance / time | help: Remove `DivisionByZero` from the docstring + +DOC502 Raised exception is not explicitly raised: `ZeroDivisionError` + --> DOC502_numpy.py:139:5 + | +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 + | +help: Remove `ZeroDivisionError` from the docstring