Skip to content

Commit 0ba7fc6

Browse files
authored
[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? -->
1 parent fa5b19d commit 0ba7fc6

File tree

3 files changed

+142
-14
lines changed

3 files changed

+142
-14
lines changed

crates/ruff_linter/resources/test/fixtures/pydocstyle/D301.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,67 @@ def make_unique_pod_id(pod_id: str) -> str | None:
3535

3636
def shouldnt_add_raw_here2():
3737
u"Sum\\mary."
38+
39+
40+
def shouldnt_add_raw_for_double_quote_docstring_contains_docstring():
41+
"""
42+
This docstring contains another double-quote docstring.
43+
44+
def foo():
45+
\"\"\"Foo.\"\"\"
46+
"""
47+
48+
49+
def shouldnt_add_raw_for_double_quote_docstring_contains_docstring2():
50+
"""
51+
This docstring contains another double-quote docstring.
52+
53+
def bar():
54+
\"""Bar.\"""
55+
56+
More content here.
57+
"""
58+
59+
60+
def shouldnt_add_raw_for_single_quote_docstring_contains_docstring():
61+
'''
62+
This docstring contains another single-quote docstring.
63+
64+
def foo():
65+
\'\'\'Foo.\'\'\'
66+
67+
More content here.
68+
'''
69+
70+
71+
def shouldnt_add_raw_for_single_quote_docstring_contains_docstring2():
72+
'''
73+
This docstring contains another single-quote docstring.
74+
75+
def bar():
76+
\'''Bar.\'''
77+
78+
More content here.
79+
'''
80+
81+
def shouldnt_add_raw_for_docstring_contains_escaped_double_triple_quotes():
82+
"""
83+
Escaped triple quote \""" or \"\"\".
84+
"""
85+
86+
def shouldnt_add_raw_for_docstring_contains_escaped_single_triple_quotes():
87+
'''
88+
Escaped triple quote \''' or \'\'\'.
89+
'''
90+
91+
92+
def should_add_raw_for_single_double_quote_escape():
93+
"""
94+
This is single quote escape \".
95+
"""
96+
97+
98+
def should_add_raw_for_single_single_quote_escape():
99+
'''
100+
This is single quote escape \'.
101+
'''

crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use memchr::memchr_iter;
2-
31
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
42
use ruff_macros::{derive_message_formats, violation};
53
use ruff_text_size::Ranged;
@@ -69,20 +67,47 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
6967
// Docstring contains at least one backslash.
7068
let body = docstring.body();
7169
let bytes = body.as_bytes();
72-
if memchr_iter(b'\\', bytes).any(|position| {
73-
let escaped_char = bytes.get(position.saturating_add(1));
74-
// Allow continuations (backslashes followed by newlines) and Unicode escapes.
75-
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'U' | b'N'))
76-
}) {
77-
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
70+
let mut offset = 0;
71+
while let Some(position) = memchr::memchr(b'\\', &bytes[offset..]) {
72+
if position + offset + 1 >= body.len() {
73+
break;
74+
}
75+
76+
let after_escape = &body[position + offset + 1..];
77+
78+
// End of Docstring.
79+
let Some(escaped_char) = &after_escape.chars().next() else {
80+
break;
81+
};
7882

79-
if !docstring.leading_quote().contains(['u', 'U']) {
80-
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
81-
"r".to_owned() + docstring.contents,
82-
docstring.range(),
83-
)));
83+
if matches!(escaped_char, '"' | '\'') {
84+
// If the next three characters are equal to """, it indicates an escaped docstring pattern.
85+
if after_escape.starts_with("\"\"\"") || after_escape.starts_with("\'\'\'") {
86+
offset += position + 3;
87+
continue;
88+
}
89+
// If the next three characters are equal to "\"\", it indicates an escaped docstring pattern.
90+
if after_escape.starts_with("\"\\\"\\\"") || after_escape.starts_with("\'\\\'\\\'") {
91+
offset += position + 5;
92+
continue;
93+
}
8494
}
8595

86-
checker.diagnostics.push(diagnostic);
96+
offset += position + escaped_char.len_utf8();
97+
98+
// Only allow continuations (backslashes followed by newlines) and Unicode escapes.
99+
if !matches!(*escaped_char, '\r' | '\n' | 'u' | 'U' | 'N') {
100+
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
101+
102+
if !docstring.leading_quote().contains(['u', 'U']) {
103+
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
104+
"r".to_owned() + docstring.contents,
105+
docstring.range(),
106+
)));
107+
}
108+
109+
checker.diagnostics.push(diagnostic);
110+
break;
111+
}
87112
}
88113
}

crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D301_D301.py.snap

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,43 @@ D301.py:37:5: D301 Use `r"""` if any backslashes in a docstring
2525
|
2626
= help: Add `r` prefix
2727

28+
D301.py:93:5: D301 [*] Use `r"""` if any backslashes in a docstring
29+
|
30+
92 | def should_add_raw_for_single_double_quote_escape():
31+
93 | """
32+
| _____^
33+
94 | | This is single quote escape \".
34+
95 | | """
35+
| |_______^ D301
36+
|
37+
= help: Add `r` prefix
2838

39+
Unsafe fix
40+
90 90 |
41+
91 91 |
42+
92 92 | def should_add_raw_for_single_double_quote_escape():
43+
93 |- """
44+
93 |+ r"""
45+
94 94 | This is single quote escape \".
46+
95 95 | """
47+
96 96 |
48+
49+
D301.py:99:5: D301 [*] Use `r"""` if any backslashes in a docstring
50+
|
51+
98 | def should_add_raw_for_single_single_quote_escape():
52+
99 | '''
53+
| _____^
54+
100 | | This is single quote escape \'.
55+
101 | | '''
56+
| |_______^ D301
57+
|
58+
= help: Add `r` prefix
59+
60+
Unsafe fix
61+
96 96 |
62+
97 97 |
63+
98 98 | def should_add_raw_for_single_single_quote_escape():
64+
99 |- '''
65+
99 |+ r'''
66+
100 100 | This is single quote escape \'.
67+
101 101 | '''

0 commit comments

Comments
 (0)