diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py index 8c84a7eecd1dd..e9106dc38507c 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py @@ -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"). + """ diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py index 223c55dff5f1e..4a50617d26be7 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py @@ -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: + """ diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index 73a42a443d0b0..1ef49ea614604 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -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(), ) { @@ -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(), }); @@ -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() @@ -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, @@ -401,12 +409,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option /// 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 { @@ -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 } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap index 3fb4608144b21..3fd044b8d8514 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap @@ -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 | """ + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap index 6871d54cc629b..53e527de24b33 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap @@ -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 | """ + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap index 26302131bd31b..9566fd691a6d4 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap @@ -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 + | + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap index 641662a53b948..71117e0c97538 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap @@ -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, + | + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap index 641662a53b948..71117e0c97538 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap @@ -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, + | +