[pydocstyle] Escaped docstring in docstring (D301 ) (#12192)

<!--
Thank you for contributing to Ruff! 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?
- 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 updates D301 rule to allow inclduing escaped docstring, e.g.
`\"""Foo.\"""` or `\"\"\"Bar.\"\"\"`, within a docstring.

Related issue: #12152 

## Test Plan

Add more test cases to D301.py and update the snapshot file.

<!-- How was it tested? -->
This commit is contained in:
ukyen 2024-07-18 23:36:05 +01:00 committed by GitHub
parent fa5b19d4b6
commit 0ba7fc63d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 143 additions and 15 deletions

View file

@ -35,3 +35,67 @@ def make_unique_pod_id(pod_id: str) -> str | None:
def shouldnt_add_raw_here2(): def shouldnt_add_raw_here2():
u"Sum\\mary." u"Sum\\mary."
def shouldnt_add_raw_for_double_quote_docstring_contains_docstring():
"""
This docstring contains another double-quote docstring.
def foo():
\"\"\"Foo.\"\"\"
"""
def shouldnt_add_raw_for_double_quote_docstring_contains_docstring2():
"""
This docstring contains another double-quote docstring.
def bar():
\"""Bar.\"""
More content here.
"""
def shouldnt_add_raw_for_single_quote_docstring_contains_docstring():
'''
This docstring contains another single-quote docstring.
def foo():
\'\'\'Foo.\'\'\'
More content here.
'''
def shouldnt_add_raw_for_single_quote_docstring_contains_docstring2():
'''
This docstring contains another single-quote docstring.
def bar():
\'''Bar.\'''
More content here.
'''
def shouldnt_add_raw_for_docstring_contains_escaped_double_triple_quotes():
"""
Escaped triple quote \""" or \"\"\".
"""
def shouldnt_add_raw_for_docstring_contains_escaped_single_triple_quotes():
'''
Escaped triple quote \''' or \'\'\'.
'''
def should_add_raw_for_single_double_quote_escape():
"""
This is single quote escape \".
"""
def should_add_raw_for_single_single_quote_escape():
'''
This is single quote escape \'.
'''

View file

@ -1,5 +1,3 @@
use memchr::memchr_iter;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -69,20 +67,47 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
// Docstring contains at least one backslash. // Docstring contains at least one backslash.
let body = docstring.body(); let body = docstring.body();
let bytes = body.as_bytes(); let bytes = body.as_bytes();
if memchr_iter(b'\\', bytes).any(|position| { let mut offset = 0;
let escaped_char = bytes.get(position.saturating_add(1)); while let Some(position) = memchr::memchr(b'\\', &bytes[offset..]) {
// Allow continuations (backslashes followed by newlines) and Unicode escapes. if position + offset + 1 >= body.len() {
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'U' | b'N')) break;
}) {
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
if !docstring.leading_quote().contains(['u', 'U']) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
)));
} }
checker.diagnostics.push(diagnostic); let after_escape = &body[position + offset + 1..];
// End of Docstring.
let Some(escaped_char) = &after_escape.chars().next() else {
break;
};
if matches!(escaped_char, '"' | '\'') {
// If the next three characters are equal to """, it indicates an escaped docstring pattern.
if after_escape.starts_with("\"\"\"") || after_escape.starts_with("\'\'\'") {
offset += position + 3;
continue;
}
// If the next three characters are equal to "\"\", it indicates an escaped docstring pattern.
if after_escape.starts_with("\"\\\"\\\"") || after_escape.starts_with("\'\\\'\\\'") {
offset += position + 5;
continue;
}
}
offset += position + escaped_char.len_utf8();
// Only allow continuations (backslashes followed by newlines) and Unicode escapes.
if !matches!(*escaped_char, '\r' | '\n' | 'u' | 'U' | 'N') {
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
if !docstring.leading_quote().contains(['u', 'U']) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);
break;
}
} }
} }

View file

@ -25,4 +25,43 @@ D301.py:37:5: D301 Use `r"""` if any backslashes in a docstring
| |
= help: Add `r` prefix = help: Add `r` prefix
D301.py:93:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
92 | def should_add_raw_for_single_double_quote_escape():
93 | """
| _____^
94 | | This is single quote escape \".
95 | | """
| |_______^ D301
|
= help: Add `r` prefix
Unsafe fix
90 90 |
91 91 |
92 92 | def should_add_raw_for_single_double_quote_escape():
93 |- """
93 |+ r"""
94 94 | This is single quote escape \".
95 95 | """
96 96 |
D301.py:99:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
98 | def should_add_raw_for_single_single_quote_escape():
99 | '''
| _____^
100 | | This is single quote escape \'.
101 | | '''
| |_______^ D301
|
= help: Add `r` prefix
Unsafe fix
96 96 |
97 97 |
98 98 | def should_add_raw_for_single_single_quote_escape():
99 |- '''
99 |+ r'''
100 100 | This is single quote escape \'.
101 101 | '''