Skip to content

Commit

Permalink
[pydocstyle] Escaped docstring in docstring (D301 ) (#12192)
Browse files Browse the repository at this point in the history
<!--
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? -->
  • Loading branch information
ukyen8 committed Jul 18, 2024
1 parent fa5b19d commit 0ba7fc6
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 14 deletions.
64 changes: 64 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pydocstyle/D301.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,67 @@ def make_unique_pod_id(pod_id: str) -> str | None:

def shouldnt_add_raw_here2():
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 \'.
'''
53 changes: 39 additions & 14 deletions crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use memchr::memchr_iter;

use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
Expand Down Expand Up @@ -69,20 +67,47 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
// Docstring contains at least one backslash.
let body = docstring.body();
let bytes = body.as_bytes();
if memchr_iter(b'\\', bytes).any(|position| {
let escaped_char = bytes.get(position.saturating_add(1));
// Allow continuations (backslashes followed by newlines) and Unicode escapes.
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'U' | b'N'))
}) {
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
let mut offset = 0;
while let Some(position) = memchr::memchr(b'\\', &bytes[offset..]) {
if position + offset + 1 >= body.len() {
break;
}

let after_escape = &body[position + offset + 1..];

// End of Docstring.
let Some(escaped_char) = &after_escape.chars().next() else {
break;
};

if !docstring.leading_quote().contains(['u', 'U']) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
)));
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;
}
}

checker.diagnostics.push(diagnostic);
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,43 @@ D301.py:37:5: D301 Use `r"""` if any backslashes in a docstring
|
= 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 | '''

0 comments on commit 0ba7fc6

Please sign in to comment.