From 966fd6f57acafb4d476c406344e16e795ecd5df7 Mon Sep 17 00:00:00 2001 From: GiGaGon <107241144+MeGaGiGaGon@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:31:36 -0700 Subject: [PATCH] [`pydoclint`] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) (#19246) ## Summary 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 Added a test case. --- .../resources/test/fixtures/pydocstyle/D.py | 7 +++ .../rules/blank_before_after_function.rs | 50 ++++++++++++------- ...__rules__pydocstyle__tests__D201_D.py.snap | 11 ++++ ...__rules__pydocstyle__tests__D202_D.py.snap | 13 ++++- ...__rules__pydocstyle__tests__D208_D.py.snap | 2 + 5 files changed, 64 insertions(+), 19 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D.py index 617231c5c8..e5b1b15c9c 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D.py @@ -722,3 +722,10 @@ def inconsistent_indent_byte_size():     Returns: """ + + +def line_continuation_chars():\ + + """No fix should be offered for D201/D202 because of the line continuation chars."""\ + + ... diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs index a3f1ab0efd..bf124a12d1 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -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 { + 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 { + 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, + ))); + } } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap index b22486d55a..98a7e7e11e 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D201_D.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D.py.snap index 8852a6a690..d234ae1d62 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D202_D.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap index c3ba66a565..acf0b91d6c 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D208_D.py.snap @@ -428,3 +428,5 @@ D.py:723:1: D208 [*] Docstring is over-indented 723 |-     Returns: 723 |+ Returns: 724 724 | """ +725 725 | +726 726 |