Move Q001-3 to AST based checker (#10312)

## Summary

Continuing with https://github.com/astral-sh/ruff/issues/7595, this PR
moves the `Q001`, `Q002`, `Q003` rules to the AST based checker.

## Test Plan

Make sure all of the existing test cases pass and verify there are no
ecosystem changes.
This commit is contained in:
Dhruv Manilawala 2024-03-23 22:59:50 +05:30 committed by GitHub
parent 0c194f55e8
commit 895d9df02f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 109 additions and 178 deletions

View file

@ -2,7 +2,7 @@ use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::codes::Rule; use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi, ruff}; use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, ruff};
/// Run lint rules over a [`StringLike`] syntax nodes. /// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) { pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
@ -23,4 +23,11 @@ pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like); flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
} }
} }
if checker.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
]) {
flake8_quotes::rules::check_string_quotes(checker, string_like);
}
} }

View file

@ -130,14 +130,6 @@ pub(crate) fn check_tokens(
flake8_quotes::rules::unnecessary_escaped_quote(&mut diagnostics, tokens, locator); flake8_quotes::rules::unnecessary_escaped_quote(&mut diagnostics, tokens, locator);
} }
if settings.rules.any_enabled(&[
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
]) {
flake8_quotes::rules::check_string_quotes(&mut diagnostics, tokens, locator, settings);
}
if settings.rules.any_enabled(&[ if settings.rules.any_enabled(&[
Rule::SingleLineImplicitStringConcatenation, Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation, Rule::MultiLineImplicitStringConcatenation,

View file

@ -257,9 +257,6 @@ impl Rule {
| Rule::TrailingWhitespace => LintSource::PhysicalLines, | Rule::TrailingWhitespace => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment Rule::AmbiguousUnicodeCharacterComment
| Rule::AvoidableEscapedQuote | Rule::AvoidableEscapedQuote
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::BlanketNOQA | Rule::BlanketNOQA
| Rule::BlanketTypeIgnore | Rule::BlanketTypeIgnore
| Rule::BlankLineAfterDecorator | Rule::BlankLineAfterDecorator

View file

@ -1,14 +1,10 @@
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::StringLike;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::lex::docstring_detection::StateMachine; use crate::checkers::ast::Checker;
use crate::settings::LinterSettings;
use super::super::settings::Quote; use super::super::settings::Quote;
@ -256,29 +252,31 @@ fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool
} }
/// Q002 /// Q002
fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> Option<Diagnostic> { fn docstring(checker: &mut Checker, range: TextRange) {
let quotes_settings = &settings.flake8_quotes; let quotes_settings = &checker.settings.flake8_quotes;
let locator = checker.locator();
let text = locator.slice(range); let text = locator.slice(range);
let trivia: Trivia = text.into(); let trivia: Trivia = text.into();
if trivia.has_empty_text() if trivia.has_empty_text()
&& text_ends_at_quote(locator, range, settings.flake8_quotes.docstring_quotes) && text_ends_at_quote(locator, range, quotes_settings.docstring_quotes)
{ {
// Fixing this would result in a one-sided multi-line docstring, which would // Fixing this would result in a one-sided multi-line docstring, which would
// introduce a syntax error. // introduce a syntax error.
return Some(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
BadQuotesDocstring { BadQuotesDocstring {
preferred_quote: quotes_settings.docstring_quotes, preferred_quote: quotes_settings.docstring_quotes,
}, },
range, range,
)); ));
return;
} }
if trivia if trivia
.raw_text .raw_text
.contains(good_docstring(quotes_settings.docstring_quotes)) .contains(good_docstring(quotes_settings.docstring_quotes))
{ {
return None; return;
} }
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
@ -302,18 +300,13 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) ->
fixed_contents, fixed_contents,
range, range,
))); )));
Some(diagnostic) checker.diagnostics.push(diagnostic);
} }
/// Q000, Q001 /// Q000, Q001
fn strings( fn strings(checker: &mut Checker, sequence: &[TextRange]) {
locator: &Locator, let quotes_settings = &checker.settings.flake8_quotes;
sequence: &[TextRange], let locator = checker.locator();
settings: &LinterSettings,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
let quotes_settings = &settings.flake8_quotes;
let trivia = sequence let trivia = sequence
.iter() .iter()
@ -377,20 +370,20 @@ fn strings(
fixed_contents, fixed_contents,
*range, *range,
))); )));
diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} else if trivia.last_quote_char != quotes_settings.inline_quotes.as_char() } else if trivia.last_quote_char != quotes_settings.inline_quotes.as_char()
// If we're not using the preferred type, only allow use to avoid escapes. // If we're not using the preferred type, only allow use to avoid escapes.
&& !relax_quote && !relax_quote
{ {
if trivia.has_empty_text() if trivia.has_empty_text()
&& text_ends_at_quote(locator, *range, settings.flake8_quotes.inline_quotes) && text_ends_at_quote(locator, *range, quotes_settings.inline_quotes)
{ {
// Fixing this would introduce a syntax error. For example, changing the initial // Fixing this would introduce a syntax error. For example, changing the initial
// single quotes to double quotes would result in a syntax error: // single quotes to double quotes would result in a syntax error:
// ```python // ```python
// ''"assert" ' SAM macro definitions ''' // ''"assert" ' SAM macro definitions '''
// ``` // ```
diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
BadQuotesInlineString { BadQuotesInlineString {
preferred_quote: quotes_settings.inline_quotes, preferred_quote: quotes_settings.inline_quotes,
}, },
@ -399,17 +392,13 @@ fn strings(
continue; continue;
} }
if text_starts_at_consecutive_quote( if text_starts_at_consecutive_quote(locator, *range, quotes_settings.inline_quotes) {
locator,
*range,
settings.flake8_quotes.inline_quotes,
) {
// Fixing this would introduce a syntax error. For example, changing the double // Fixing this would introduce a syntax error. For example, changing the double
// doubles to single quotes would result in a syntax error: // doubles to single quotes would result in a syntax error:
// ```python // ```python
// ''"assert" ' SAM macro definitions ''' // ''"assert" ' SAM macro definitions '''
// ``` // ```
diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
BadQuotesInlineString { BadQuotesInlineString {
preferred_quote: quotes_settings.inline_quotes, preferred_quote: quotes_settings.inline_quotes,
}, },
@ -436,120 +425,34 @@ fn strings(
fixed_contents, fixed_contents,
*range, *range,
))); )));
diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
diagnostics
}
/// A builder for the f-string range.
///
/// For now, this is limited to the outermost f-string and doesn't support
/// nested f-strings.
#[derive(Debug, Default)]
struct FStringRangeBuilder {
start_location: TextSize,
end_location: TextSize,
nesting: u32,
}
impl FStringRangeBuilder {
fn visit_token(&mut self, token: &Tok, range: TextRange) {
match token {
Tok::FStringStart(_) => {
if self.nesting == 0 {
self.start_location = range.start();
}
self.nesting += 1;
}
Tok::FStringEnd => {
self.nesting = self.nesting.saturating_sub(1);
if self.nesting == 0 {
self.end_location = range.end();
}
}
_ => {}
}
}
/// Returns `true` if the lexer is currently inside of a f-string.
///
/// It'll return `false` once the `FStringEnd` token for the outermost
/// f-string is visited.
const fn in_fstring(&self) -> bool {
self.nesting > 0
}
/// Returns the complete range of the previously visited f-string.
///
/// This method should only be called once the lexer is outside of any
/// f-string otherwise it might return an invalid range.
///
/// It doesn't consume the builder because there can be multiple f-strings
/// throughout the source code.
fn finish(&self) -> TextRange {
debug_assert!(!self.in_fstring());
TextRange::new(self.start_location, self.end_location)
}
} }
/// Generate `flake8-quote` diagnostics from a token stream. /// Generate `flake8-quote` diagnostics from a token stream.
pub(crate) fn check_string_quotes( pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike) {
diagnostics: &mut Vec<Diagnostic>, // If the string is part of a f-string, ignore it.
lxr: &[LexResult], if checker
locator: &Locator, .indexer()
settings: &LinterSettings, .fstring_ranges()
) { .outermost(string_like.start())
// Keep track of sequences of strings, which represent implicit string .is_some_and(|outer| outer.start() < string_like.start() && string_like.end() < outer.end())
// concatenation, and should thus be handled as a single unit. {
let mut sequence = vec![]; return;
let mut state_machine = StateMachine::default();
let mut fstring_range_builder = FStringRangeBuilder::default();
for &(ref tok, range) in lxr.iter().flatten() {
fstring_range_builder.visit_token(tok, range);
if fstring_range_builder.in_fstring() {
continue;
}
let is_docstring = state_machine.consume(tok);
// If this is a docstring, consume the existing sequence, then consume the
// docstring, then move on.
if is_docstring {
if !sequence.is_empty() {
diagnostics.extend(strings(locator, &sequence, settings));
sequence.clear();
}
if let Some(diagnostic) = docstring(locator, range, settings) {
diagnostics.push(diagnostic);
}
} else {
match tok {
Tok::String { .. } => {
// If this is a string, add it to the sequence.
sequence.push(range);
}
Tok::FStringEnd => {
// If this is the end of an f-string, add the entire f-string
// range to the sequence.
sequence.push(fstring_range_builder.finish());
}
Tok::Comment(..) | Tok::NonLogicalNewline => continue,
_ => {
// Otherwise, consume the sequence.
if !sequence.is_empty() {
diagnostics.extend(strings(locator, &sequence, settings));
sequence.clear();
}
}
}
}
} }
// If we have an unterminated sequence, consume it. let ranges: Vec<TextRange> = match string_like {
if !sequence.is_empty() { StringLike::String(node) => node.value.iter().map(Ranged::range).collect(),
diagnostics.extend(strings(locator, &sequence, settings)); StringLike::Bytes(node) => node.value.iter().map(Ranged::range).collect(),
sequence.clear(); StringLike::FString(node) => node.value.iter().map(Ranged::range).collect(),
};
if checker.semantic().in_docstring() {
for range in ranges {
docstring(checker, range);
}
} else {
strings(checker, &ranges);
} }
} }

View file

@ -10,14 +10,22 @@ docstring_singles_mixed_quotes_class_var_1.py:2:5: Q002 Single quote docstring f
| |
= help: Replace single quotes docstring with double quotes = help: Replace single quotes docstring with double quotes
docstring_singles_mixed_quotes_class_var_1.py:2:7: Q000 Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_1.py:2:33: Q002 [*] Single quote docstring found but double quotes preferred
| |
1 | class SingleLineDocstrings(): 1 | class SingleLineDocstrings():
2 | ''"Start with empty string" ' and lint docstring safely' 2 | ''"Start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
3 | ''' Not a docstring ''' 3 | ''' Not a docstring '''
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix
1 1 | class SingleLineDocstrings():
2 |- ''"Start with empty string" ' and lint docstring safely'
2 |+ ''"Start with empty string" " and lint docstring safely"
3 3 | ''' Not a docstring '''
4 4 |
5 5 | def foo(self, bar='''not a docstring'''):
docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring found but double quotes preferred docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring found but double quotes preferred
| |
@ -28,14 +36,24 @@ docstring_singles_mixed_quotes_class_var_1.py:6:9: Q002 Single quote docstring f
| |
= help: Replace single quotes docstring with double quotes = help: Replace single quotes docstring with double quotes
docstring_singles_mixed_quotes_class_var_1.py:6:11: Q000 Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_1.py:6:37: Q002 [*] Single quote docstring found but double quotes preferred
| |
5 | def foo(self, bar='''not a docstring'''): 5 | def foo(self, bar='''not a docstring'''):
6 | ''"Start with empty string" ' and lint docstring safely' 6 | ''"Start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
7 | pass 7 | pass
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix
3 3 | ''' Not a docstring '''
4 4 |
5 5 | def foo(self, bar='''not a docstring'''):
6 |- ''"Start with empty string" ' and lint docstring safely'
6 |+ ''"Start with empty string" " and lint docstring safely"
7 7 | pass
8 8 |
9 9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass
docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring found but double quotes preferred docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring found but double quotes preferred
| |
@ -46,11 +64,18 @@ docstring_singles_mixed_quotes_class_var_1.py:9:29: Q002 Single quote docstring
| |
= help: Replace single quotes docstring with double quotes = help: Replace single quotes docstring with double quotes
docstring_singles_mixed_quotes_class_var_1.py:9:31: Q000 Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_1.py:9:57: Q002 [*] Single quote docstring found but double quotes preferred
| |
7 | pass 7 | pass
8 | 8 |
9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass 9 | class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix
6 6 | ''"Start with empty string" ' and lint docstring safely'
7 7 | pass
8 8 |
9 |- class Nested(foo()[:]): ''"Start with empty string" ' and lint docstring safely'; pass
9 |+ class Nested(foo()[:]): ''"Start with empty string" " and lint docstring safely"; pass

View file

@ -18,19 +18,19 @@ docstring_singles_mixed_quotes_class_var_2.py:2:5: Q002 [*] Single quote docstri
4 4 | 4 4 |
5 5 | def foo(self, bar='''not a docstring'''): 5 5 | def foo(self, bar='''not a docstring'''):
docstring_singles_mixed_quotes_class_var_2.py:2:13: Q000 [*] Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_2.py:2:40: Q002 [*] Single quote docstring found but double quotes preferred
| |
1 | class SingleLineDocstrings(): 1 | class SingleLineDocstrings():
2 | 'Do not'" start with empty string" ' and lint docstring safely' 2 | 'Do not'" start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
3 | ''' Not a docstring ''' 3 | ''' Not a docstring '''
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix Safe fix
1 1 | class SingleLineDocstrings(): 1 1 | class SingleLineDocstrings():
2 |- 'Do not'" start with empty string" ' and lint docstring safely' 2 |- 'Do not'" start with empty string" ' and lint docstring safely'
2 |+ 'Do not'' start with empty string' ' and lint docstring safely' 2 |+ 'Do not'" start with empty string" " and lint docstring safely"
3 3 | ''' Not a docstring ''' 3 3 | ''' Not a docstring '''
4 4 | 4 4 |
5 5 | def foo(self, bar='''not a docstring'''): 5 5 | def foo(self, bar='''not a docstring'''):
@ -54,21 +54,21 @@ docstring_singles_mixed_quotes_class_var_2.py:6:9: Q002 [*] Single quote docstri
8 8 | 8 8 |
9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass
docstring_singles_mixed_quotes_class_var_2.py:6:17: Q000 [*] Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_2.py:6:44: Q002 [*] Single quote docstring found but double quotes preferred
| |
5 | def foo(self, bar='''not a docstring'''): 5 | def foo(self, bar='''not a docstring'''):
6 | 'Do not'" start with empty string" ' and lint docstring safely' 6 | 'Do not'" start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
7 | pass 7 | pass
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix Safe fix
3 3 | ''' Not a docstring ''' 3 3 | ''' Not a docstring '''
4 4 | 4 4 |
5 5 | def foo(self, bar='''not a docstring'''): 5 5 | def foo(self, bar='''not a docstring'''):
6 |- 'Do not'" start with empty string" ' and lint docstring safely' 6 |- 'Do not'" start with empty string" ' and lint docstring safely'
6 |+ 'Do not'' start with empty string' ' and lint docstring safely' 6 |+ 'Do not'" start with empty string" " and lint docstring safely"
7 7 | pass 7 7 | pass
8 8 | 8 8 |
9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass
@ -89,18 +89,18 @@ docstring_singles_mixed_quotes_class_var_2.py:9:29: Q002 [*] Single quote docstr
9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass
9 |+ class Nested(foo()[:]): "Do not"" start with empty string" ' and lint docstring safely'; pass 9 |+ class Nested(foo()[:]): "Do not"" start with empty string" ' and lint docstring safely'; pass
docstring_singles_mixed_quotes_class_var_2.py:9:37: Q000 [*] Double quotes found but single quotes preferred docstring_singles_mixed_quotes_class_var_2.py:9:64: Q002 [*] Single quote docstring found but double quotes preferred
| |
7 | pass 7 | pass
8 | 8 |
9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 | class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix Safe fix
6 6 | 'Do not'" start with empty string" ' and lint docstring safely' 6 6 | 'Do not'" start with empty string" ' and lint docstring safely'
7 7 | pass 7 7 | pass
8 8 | 8 8 |
9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass 9 |- class Nested(foo()[:]): 'Do not'" start with empty string" ' and lint docstring safely'; pass
9 |+ class Nested(foo()[:]): 'Do not'' start with empty string' ' and lint docstring safely'; pass 9 |+ class Nested(foo()[:]): 'Do not'" start with empty string" " and lint docstring safely"; pass

View file

@ -10,14 +10,21 @@ docstring_singles_mixed_quotes_module_singleline_var_1.py:1:1: Q002 Single quote
| |
= help: Replace single quotes docstring with double quotes = help: Replace single quotes docstring with double quotes
docstring_singles_mixed_quotes_module_singleline_var_1.py:1:3: Q000 Double quotes found but single quotes preferred docstring_singles_mixed_quotes_module_singleline_var_1.py:1:29: Q002 [*] Single quote docstring found but double quotes preferred
| |
1 | ''"Start with empty string" ' and lint docstring safely' 1 | ''"Start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
2 | 2 |
3 | def foo(): 3 | def foo():
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix
1 |-''"Start with empty string" ' and lint docstring safely'
1 |+''"Start with empty string" " and lint docstring safely"
2 2 |
3 3 | def foo():
4 4 | pass
docstring_singles_mixed_quotes_module_singleline_var_1.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred docstring_singles_mixed_quotes_module_singleline_var_1.py:5:1: Q001 [*] Double quote multiline found but single quotes preferred
| |

View file

@ -17,18 +17,18 @@ docstring_singles_mixed_quotes_module_singleline_var_2.py:1:1: Q002 [*] Single q
3 3 | def foo(): 3 3 | def foo():
4 4 | pass 4 4 | pass
docstring_singles_mixed_quotes_module_singleline_var_2.py:1:9: Q000 [*] Double quotes found but single quotes preferred docstring_singles_mixed_quotes_module_singleline_var_2.py:1:36: Q002 [*] Single quote docstring found but double quotes preferred
| |
1 | 'Do not'" start with empty string" ' and lint docstring safely' 1 | 'Do not'" start with empty string" ' and lint docstring safely'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q000 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002
2 | 2 |
3 | def foo(): 3 | def foo():
| |
= help: Replace double quotes with single quotes = help: Replace single quotes docstring with double quotes
Safe fix Safe fix
1 |-'Do not'" start with empty string" ' and lint docstring safely' 1 |-'Do not'" start with empty string" ' and lint docstring safely'
1 |+'Do not'' start with empty string' ' and lint docstring safely' 1 |+'Do not'" start with empty string" " and lint docstring safely"
2 2 | 2 2 |
3 3 | def foo(): 3 3 | def foo():
4 4 | pass 4 4 | pass