Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,16 @@ 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
44 changes: 38 additions & 6 deletions crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,13 +476,45 @@ fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedNam
/// ```
fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = 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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about something more like this for this function?

fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
    let mut entries: Vec<QualifiedName> = Vec::new();
    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));
                        }
                    }
                }
            }
        }
    }
    entries
}

This is closer to the parse_entries_numpy down below and seems to work on the new test case when I tried it locally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is significantly better, thank you! We will have to see what the ecosystem results will look like.

// Determine the indentation of the entries from the first non-empty line.
// Google-style entries are indented relative to the "Raises:" header, e.g.:
// " ValueError: explanation".
let lines = content.lines();
let mut expected_indent: Option<&str> = None;

for raw in lines {
let line = raw.trim_end_matches('\r');

// Stop if we encounter an unindented line or a Sphinx directive starting with ".. ".
if !line.trim().is_empty() {
// Compute indentation of current line
let indent_len = line.len() - line.trim_start().len();
let indent = &line[..indent_len];

// If this looks like a Sphinx directive or any unindented content, the section ends
if indent_len == 0 || line.trim_start().starts_with(".. ") {
break;
}

// Establish expected indentation based on the first valid entry line
if expected_indent.is_none() {
expected_indent = Some(indent);
} else if Some(indent) != expected_indent {
// Different indentation likely starts a new sub-block; stop collecting
break;
}

// Parse only lines that contain a colon and where the token before the colon is non-empty
if let Some(colon_idx) = line.find(':') {
let entry = line[..colon_idx].trim();
if !entry.is_empty() {
entries.push(QualifiedName::user_defined(entry));
}
}
}
}

entries
}

Expand Down
Loading