Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | '''
Loading