Skip to content

Commit

Permalink
Disambiguate argument descriptors from section headers
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jan 8, 2024
1 parent d5a439c commit 149aa44
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 8 deletions.
18 changes: 18 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,21 @@ def f(self, /, arg1: int) -> None:
Args:
arg1: some description of arg
"""


def select_data(
query: str,
args: tuple,
database: str,
auto_save: bool,
) -> None:
"""This function has an argument `args`, which shouldn't be mistaken for a section.
Args:
query:
Query template.
args:
A list of arguments.
database:
Which database to connect to ("origin" or "destination").
"""
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,29 @@ def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""


def lowercase_sub_section_header():
"""Below, `returns:` should _not_ be considered a section header.
Args:
Here's a note.
returns:
"""


def titlecase_sub_section_header():
"""Below, `Returns:` should be considered a section header.
Args:
Here's a note.
Returns:
"""
45 changes: 38 additions & 7 deletions crates/ruff_linter/src/docstrings/sections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,17 @@ impl<'a> SectionContexts<'a> {
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
let indent_size = indent.text_len();

let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
let section_name = leading_words(&line);
let section_name_size = section_name.text_len();

if is_docstring_section(
&line,
section_name_range,
indent_size,
section_name_size,
section_kind,
last.as_ref(),
previous_line.as_ref(),
lines.peek(),
) {
Expand All @@ -170,7 +174,8 @@ impl<'a> SectionContexts<'a> {

last = Some(SectionContextData {
kind: section_kind,
name_range: section_name_range + line.start(),
indent_size: indent.text_len(),
name_range: TextRange::at(line.start() + indent_size, section_name_size),
range: TextRange::empty(line.start()),
summary_full_end: line.full_end(),
});
Expand Down Expand Up @@ -204,8 +209,8 @@ impl<'a> SectionContexts<'a> {
}

impl<'a> IntoIterator for &'a SectionContexts<'a> {
type IntoIter = SectionContextsIter<'a>;
type Item = SectionContext<'a>;
type IntoIter = SectionContextsIter<'a>;

fn into_iter(self) -> Self::IntoIter {
self.iter()
Expand Down Expand Up @@ -257,6 +262,9 @@ impl ExactSizeIterator for SectionContextsIter<'_> {}
struct SectionContextData {
kind: SectionKind,

/// The size of the indentation of the section name.
indent_size: TextSize,

/// Range of the section name, relative to the [`Docstring::body`]
name_range: TextRange,

Expand Down Expand Up @@ -401,12 +409,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
/// Check if the suspected context is really a section header.
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
indent_size: TextSize,
section_name_size: TextSize,
section_kind: SectionKind,
previous_section: Option<&SectionContextData>,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
Expand Down Expand Up @@ -439,5 +450,25 @@ fn is_docstring_section(
return false;
}

// Determine if this is a sub-section within another section, like `args` in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
if section_kind.as_str() != verbatim {
return false;
}
}
}

true
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,27 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns")
148 148 | A value of some sort.
149 149 |

sections.py:558:5: D214 [*] Section is over-indented ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D214
|
= help: Remove over-indentation from "Returns"

Safe fix
560 560 | Args:
561 561 | Here's a note.
562 562 |
563 |- Returns:
563 |+ Returns:
564 564 | """
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,74 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
531 532 | """
532 533 |

sections.py:548:5: D407 [*] Missing dashed underline after section ("Args")
|
547 | def lowercase_sub_section_header():
548 | """Below, `returns:` should _not_ be considered a section header.
| _____^
549 | |
550 | | Args:
551 | | Here's a note.
552 | |
553 | | returns:
554 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"

ℹ Safe fix
548 548 | """Below, `returns:` should _not_ be considered a section header.
549 549 |
550 550 | Args:
551 |+ ----
551 552 | Here's a note.
552 553 |
553 554 | returns:

sections.py:558:5: D407 [*] Missing dashed underline after section ("Args")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"

ℹ Safe fix
558 558 | """Below, `Returns:` should be considered a section header.
559 559 |
560 560 | Args:
561 |+ ----
561 562 | Here's a note.
562 563 |
563 564 | Returns:

sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Returns"

ℹ Safe fix
561 561 | Here's a note.
562 562 |
563 563 | Returns:
564 |+ -------
564 565 | """


Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,18 @@ sections.py:261:5: D414 Section has no content ("Returns")
| |_______^ D414
|

sections.py:558:5: D414 Section has no content ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D414
|


Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,12 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|

D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|


Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,12 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|

D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|


0 comments on commit 149aa44

Please sign in to comment.