[pydoclint] Fix SyntaxError from fixes with line continuations (D201, D202) (#19246)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

This PR fixes #7172 by suppressing the fixes for
[docstring-missing-returns
(DOC201)](https://docs.astral.sh/ruff/rules/docstring-missing-returns/#docstring-missing-returns-doc201)
/ [docstring-extraneous-returns
(DOC202)](https://docs.astral.sh/ruff/rules/docstring-extraneous-returns/#docstring-extraneous-returns-doc202)
if there is a surrounding line continuation character `\` that would
make the fix cause a syntax error.

To do this, the lints are changed from `AlwaysFixableViolation` to
`Violation` with `FixAvailability::Sometimes`.

In the case of `DOC201`, the fix is not given if the non-break line ends
in a line continuation character `\`. Note that lines are iterated in
reverse from the docstring to the function definition.

In the case of `DOC202`, the fix is not given if the docstring ends with
a line continuation character `\`.

## Test Plan

<!-- How was it tested? -->

Added a test case.
This commit is contained in:
GiGaGon 2025-07-14 10:31:36 -07:00 committed by GitHub
parent 4f60f0e925
commit 966fd6f57a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 64 additions and 19 deletions

View file

@ -10,7 +10,7 @@ use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::docstrings::Docstring;
use crate::registry::Rule;
use crate::{AlwaysFixableViolation, Edit, Fix};
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for docstrings on functions that are separated by one or more blank
@ -42,15 +42,17 @@ pub(crate) struct BlankLineBeforeFunction {
num_lines: usize,
}
impl AlwaysFixableViolation for BlankLineBeforeFunction {
impl Violation for BlankLineBeforeFunction {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let BlankLineBeforeFunction { num_lines } = self;
format!("No blank lines allowed before function docstring (found {num_lines})")
}
fn fix_title(&self) -> String {
"Remove blank line(s) before function docstring".to_string()
fn fix_title(&self) -> Option<String> {
Some("Remove blank line(s) before function docstring".to_string())
}
}
@ -86,15 +88,17 @@ pub(crate) struct BlankLineAfterFunction {
num_lines: usize,
}
impl AlwaysFixableViolation for BlankLineAfterFunction {
impl Violation for BlankLineAfterFunction {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let BlankLineAfterFunction { num_lines } = self;
format!("No blank lines allowed after function docstring (found {num_lines})")
}
fn fix_title(&self) -> String {
"Remove blank line(s) after function docstring".to_string()
fn fix_title(&self) -> Option<String> {
Some("Remove blank line(s) after function docstring".to_string())
}
}
@ -115,12 +119,14 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri
let mut lines = UniversalNewlineIterator::with_offset(before, function.start()).rev();
let mut blank_lines_before = 0usize;
let mut blank_lines_start = lines.next().map(|l| l.end()).unwrap_or_default();
let mut start_is_line_continuation = false;
for line in lines {
if line.trim().is_empty() {
blank_lines_before += 1;
blank_lines_start = line.start();
} else {
start_is_line_continuation = line.ends_with('\\');
break;
}
}
@ -132,11 +138,14 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri
},
docstring.range(),
);
// Delete the blank line before the docstring.
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
blank_lines_start,
docstring.line_start(),
)));
// Do not offer fix if a \ would cause it to be a syntax error
if !start_is_line_continuation {
// Delete the blank line before the docstring.
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
blank_lines_start,
docstring.line_start(),
)));
}
}
}
@ -156,7 +165,9 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri
// Count the number of blank lines after the docstring.
let mut blank_lines_after = 0usize;
let mut lines = UniversalNewlineIterator::with_offset(after, docstring.end()).peekable();
let first_line_end = lines.next().map(|l| l.end()).unwrap_or_default();
let first_line = lines.next();
let first_line_line_continuation = first_line.as_ref().is_some_and(|l| l.ends_with('\\'));
let first_line_end = first_line.map(|l| l.end()).unwrap_or_default();
let mut blank_lines_end = first_line_end;
while let Some(line) = lines.peek() {
@ -185,11 +196,14 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri
},
docstring.range(),
);
// Delete the blank line after the docstring.
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
first_line_end,
blank_lines_end,
)));
// Do not offer fix if a \ would cause it to be a syntax error
if !first_line_line_continuation {
// Delete the blank line after the docstring.
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
first_line_end,
blank_lines_end,
)));
}
}
}
}

View file

@ -82,3 +82,14 @@ D.py:568:5: D201 [*] No blank lines allowed before function docstring (found 1)
568 567 | """Trailing and leading space.
569 568 |
570 569 | More content.
D.py:729:5: D201 No blank lines allowed before function docstring (found 1)
|
727 | def line_continuation_chars():\
728 |
729 | """No fix should be offered for D201/D202 because of the line continuation chars."""\
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D201
730 |
731 | ...
|
= help: Remove blank line(s) before function docstring

View file

@ -85,4 +85,15 @@ D.py:568:5: D202 [*] No blank lines allowed after function docstring (found 1)
572 |-
573 572 | pass
574 573 |
575 574 |
575 574 |
D.py:729:5: D202 No blank lines allowed after function docstring (found 1)
|
727 | def line_continuation_chars():\
728 |
729 | """No fix should be offered for D201/D202 because of the line continuation chars."""\
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D202
730 |
731 | ...
|
= help: Remove blank line(s) after function docstring

View file

@ -428,3 +428,5 @@ D.py:723:1: D208 [*] Docstring is over-indented
723 |-     Returns:
723 |+ Returns:
724 724 | """
725 725 |
726 726 |