mirror of https://github.com/astral-sh/ruff
Allow implicit multiline strings with internal quotes to use non-preferred quote (#2416)
As an example, if you have `single` as your preferred style, we'll now allow this:
```py
assert s.to_python(123) == (
"123 info=SerializationInfo(include=None, exclude=None, mode='python', by_alias=True, exclude_unset=False, "
"exclude_defaults=False, exclude_none=False, round_trip=False)"
)
```
Previously, the second line of the implicit string concatenation would be flagged as invalid, despite the _first_ line requiring double quotes. (Note that we'll accept either single or double quotes for that second line.)
Mechanically, this required that we process sequences of `Tok::String` rather than a single `Tok::String` at a time. Prior to iterating over the strings in the sequence, we check if any of them require the non-preferred quote style; if so, we let _any_ of them use it.
Closes #2400.
This commit is contained in:
parent
1dd9ccf7f6
commit
fbf231e1b8
|
|
@ -1,2 +1,4 @@
|
||||||
this_should_be_linted = "double quote string"
|
this_should_be_linted = "double quote string"
|
||||||
this_should_be_linted = u"double quote string"
|
this_should_be_linted = u"double quote string"
|
||||||
|
this_should_be_linted = f"double quote string"
|
||||||
|
this_should_be_linted = f"double {'quote'} string"
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,8 @@ this_is_fine = '"This" is a \'string\''
|
||||||
this_is_fine = "This is a 'string'"
|
this_is_fine = "This is a 'string'"
|
||||||
this_is_fine = "\"This\" is a 'string'"
|
this_is_fine = "\"This\" is a 'string'"
|
||||||
this_is_fine = r'This is a \'string\''
|
this_is_fine = r'This is a \'string\''
|
||||||
|
this_is_fine = R'This is a \'string\''
|
||||||
|
this_should_raise = (
|
||||||
|
'This is a'
|
||||||
|
'\'string\''
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
x = (
|
||||||
|
"This"
|
||||||
|
"is"
|
||||||
|
"not"
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
"This" \
|
||||||
|
"is" \
|
||||||
|
"not"
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
"This"
|
||||||
|
"is 'actually'"
|
||||||
|
"fine"
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
"This" \
|
||||||
|
"is 'actually'" \
|
||||||
|
"fine"
|
||||||
|
)
|
||||||
|
|
||||||
|
if True:
|
||||||
|
"This can use 'double' quotes"
|
||||||
|
"But this needs to be changed"
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
this_should_be_linted = 'single quote string'
|
this_should_be_linted = 'single quote string'
|
||||||
this_should_be_linted = u'double quote string'
|
this_should_be_linted = u'double quote string'
|
||||||
|
this_should_be_linted = f'double quote string'
|
||||||
|
this_should_be_linted = f'double {"quote"} string'
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,8 @@ this_is_fine = "'This' is a \"string\""
|
||||||
this_is_fine = 'This is a "string"'
|
this_is_fine = 'This is a "string"'
|
||||||
this_is_fine = '\'This\' is a "string"'
|
this_is_fine = '\'This\' is a "string"'
|
||||||
this_is_fine = r"This is a \"string\""
|
this_is_fine = r"This is a \"string\""
|
||||||
|
this_is_fine = R"This is a \"string\""
|
||||||
|
this_should_raise = (
|
||||||
|
"This is a"
|
||||||
|
"\"string\""
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
x = (
|
||||||
|
'This'
|
||||||
|
'is'
|
||||||
|
'not'
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
'This' \
|
||||||
|
'is' \
|
||||||
|
'not'
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
'This'
|
||||||
|
'is "actually"'
|
||||||
|
'fine'
|
||||||
|
)
|
||||||
|
|
||||||
|
x = (
|
||||||
|
'This' \
|
||||||
|
'is "actually"' \
|
||||||
|
'fine'
|
||||||
|
)
|
||||||
|
|
||||||
|
if True:
|
||||||
|
'This can use "single" quotes'
|
||||||
|
'But this needs to be changed'
|
||||||
|
|
@ -48,77 +48,73 @@ pub fn check_tokens(
|
||||||
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
|
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
|
||||||
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
|
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
|
||||||
|
|
||||||
let mut state_machine = StateMachine::default();
|
if enforce_ambiguous_unicode_character
|
||||||
for &(start, ref tok, end) in tokens.iter().flatten() {
|
|| enforce_commented_out_code
|
||||||
let is_docstring = if enforce_ambiguous_unicode_character || enforce_quotes {
|
|| enforce_invalid_escape_sequence
|
||||||
state_machine.consume(tok)
|
{
|
||||||
} else {
|
let mut state_machine = StateMachine::default();
|
||||||
false
|
for &(start, ref tok, end) in tokens.iter().flatten() {
|
||||||
};
|
let is_docstring = if enforce_ambiguous_unicode_character {
|
||||||
|
state_machine.consume(tok)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
// RUF001, RUF002, RUF003
|
// RUF001, RUF002, RUF003
|
||||||
if enforce_ambiguous_unicode_character {
|
if enforce_ambiguous_unicode_character {
|
||||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
||||||
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
|
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
|
||||||
locator,
|
locator,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
if matches!(tok, Tok::String { .. }) {
|
if matches!(tok, Tok::String { .. }) {
|
||||||
if is_docstring {
|
if is_docstring {
|
||||||
Context::Docstring
|
Context::Docstring
|
||||||
|
} else {
|
||||||
|
Context::String
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Context::String
|
Context::Comment
|
||||||
}
|
},
|
||||||
} else {
|
settings,
|
||||||
Context::Comment
|
autofix,
|
||||||
},
|
));
|
||||||
settings,
|
}
|
||||||
autofix,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// flake8-quotes
|
// eradicate
|
||||||
if enforce_quotes {
|
if enforce_commented_out_code {
|
||||||
if matches!(tok, Tok::String { .. }) {
|
if matches!(tok, Tok::Comment(_)) {
|
||||||
if let Some(diagnostic) = flake8_quotes::rules::quotes(
|
if let Some(diagnostic) =
|
||||||
locator,
|
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
|
||||||
start,
|
{
|
||||||
end,
|
|
||||||
is_docstring,
|
|
||||||
settings,
|
|
||||||
autofix,
|
|
||||||
) {
|
|
||||||
if settings.rules.enabled(diagnostic.kind.rule()) {
|
|
||||||
diagnostics.push(diagnostic);
|
diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// eradicate
|
// W605
|
||||||
if enforce_commented_out_code {
|
if enforce_invalid_escape_sequence {
|
||||||
if matches!(tok, Tok::Comment(_)) {
|
if matches!(tok, Tok::String { .. }) {
|
||||||
if let Some(diagnostic) =
|
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
|
||||||
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
|
locator,
|
||||||
{
|
start,
|
||||||
diagnostics.push(diagnostic);
|
end,
|
||||||
|
matches!(autofix, flags::Autofix::Enabled)
|
||||||
|
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// W605
|
// Q001, Q002, Q003
|
||||||
if enforce_invalid_escape_sequence {
|
if enforce_quotes {
|
||||||
if matches!(tok, Tok::String { .. }) {
|
diagnostics.extend(
|
||||||
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
|
flake8_quotes::rules::from_tokens(tokens, locator, settings, autofix)
|
||||||
locator,
|
.into_iter()
|
||||||
start,
|
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||||
end,
|
);
|
||||||
matches!(autofix, flags::Autofix::Enabled)
|
|
||||||
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISC001, ISC002
|
// ISC001, ISC002
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ mod tests {
|
||||||
|
|
||||||
#[test_case(Path::new("doubles.py"))]
|
#[test_case(Path::new("doubles.py"))]
|
||||||
#[test_case(Path::new("doubles_escaped.py"))]
|
#[test_case(Path::new("doubles_escaped.py"))]
|
||||||
|
#[test_case(Path::new("doubles_implicit.py"))]
|
||||||
#[test_case(Path::new("doubles_multiline_string.py"))]
|
#[test_case(Path::new("doubles_multiline_string.py"))]
|
||||||
#[test_case(Path::new("doubles_noqa.py"))]
|
#[test_case(Path::new("doubles_noqa.py"))]
|
||||||
#[test_case(Path::new("doubles_wrapped.py"))]
|
#[test_case(Path::new("doubles_wrapped.py"))]
|
||||||
|
|
@ -47,6 +48,7 @@ mod tests {
|
||||||
|
|
||||||
#[test_case(Path::new("singles.py"))]
|
#[test_case(Path::new("singles.py"))]
|
||||||
#[test_case(Path::new("singles_escaped.py"))]
|
#[test_case(Path::new("singles_escaped.py"))]
|
||||||
|
#[test_case(Path::new("singles_implicit.py"))]
|
||||||
#[test_case(Path::new("singles_multiline_string.py"))]
|
#[test_case(Path::new("singles_multiline_string.py"))]
|
||||||
#[test_case(Path::new("singles_noqa.py"))]
|
#[test_case(Path::new("singles_noqa.py"))]
|
||||||
#[test_case(Path::new("singles_wrapped.py"))]
|
#[test_case(Path::new("singles_wrapped.py"))]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
use rustpython_ast::Location;
|
use rustpython_ast::Location;
|
||||||
|
use rustpython_parser::lexer::{LexResult, Tok};
|
||||||
|
|
||||||
use super::settings::Quote;
|
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::fix::Fix;
|
use crate::fix::Fix;
|
||||||
|
use crate::lex::docstring_detection::StateMachine;
|
||||||
use crate::registry::{Diagnostic, Rule};
|
use crate::registry::{Diagnostic, Rule};
|
||||||
use crate::settings::{flags, Settings};
|
use crate::settings::{flags, Settings};
|
||||||
use crate::source_code::Locator;
|
use crate::source_code::Locator;
|
||||||
use crate::violations;
|
use crate::violations;
|
||||||
|
|
||||||
|
use super::settings::Quote;
|
||||||
|
|
||||||
fn good_single(quote: &Quote) -> char {
|
fn good_single(quote: &Quote) -> char {
|
||||||
match quote {
|
match quote {
|
||||||
Quote::Single => '\'',
|
Quote::Single => '\'',
|
||||||
|
|
@ -43,178 +46,302 @@ fn good_docstring(quote: &Quote) -> &str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quotes(
|
struct Trivia<'a> {
|
||||||
|
last_quote_char: char,
|
||||||
|
prefix: &'a str,
|
||||||
|
raw_text: &'a str,
|
||||||
|
is_multiline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for Trivia<'a> {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
// Remove any prefixes (e.g., remove `u` from `u"foo"`).
|
||||||
|
let last_quote_char = value.chars().last().unwrap();
|
||||||
|
let first_quote_char = value.find(last_quote_char).unwrap();
|
||||||
|
let prefix = &value[..first_quote_char];
|
||||||
|
let raw_text = &value[first_quote_char..];
|
||||||
|
|
||||||
|
// Determine if the string is multiline-based.
|
||||||
|
let is_multiline = if raw_text.len() >= 3 {
|
||||||
|
let mut chars = raw_text.chars();
|
||||||
|
let first = chars.next().unwrap();
|
||||||
|
let second = chars.next().unwrap();
|
||||||
|
let third = chars.next().unwrap();
|
||||||
|
first == second && second == third
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
last_quote_char,
|
||||||
|
prefix,
|
||||||
|
raw_text,
|
||||||
|
is_multiline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Q003
|
||||||
|
fn docstring(
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
start: Location,
|
start: Location,
|
||||||
end: Location,
|
end: Location,
|
||||||
is_docstring: bool,
|
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
autofix: flags::Autofix,
|
autofix: flags::Autofix,
|
||||||
) -> Option<Diagnostic> {
|
) -> Option<Diagnostic> {
|
||||||
let quotes_settings = &settings.flake8_quotes;
|
let quotes_settings = &settings.flake8_quotes;
|
||||||
|
|
||||||
let text = locator.slice_source_code_range(&Range::new(start, end));
|
let text = locator.slice_source_code_range(&Range::new(start, end));
|
||||||
|
let trivia: Trivia = text.into();
|
||||||
|
|
||||||
// Remove any prefixes (e.g., remove `u` from `u"foo"`).
|
if trivia
|
||||||
let last_quote_char = text.chars().last().unwrap();
|
.raw_text
|
||||||
let first_quote_char = text.find(last_quote_char).unwrap();
|
.contains(good_docstring("es_settings.docstring_quotes))
|
||||||
let prefix = &text[..first_quote_char].to_lowercase();
|
{
|
||||||
let raw_text = &text[first_quote_char..];
|
return None;
|
||||||
|
|
||||||
// Determine if the string is multiline-based.
|
|
||||||
let is_multiline = if raw_text.len() >= 3 {
|
|
||||||
let mut chars = raw_text.chars();
|
|
||||||
let first = chars.next().unwrap();
|
|
||||||
let second = chars.next().unwrap();
|
|
||||||
let third = chars.next().unwrap();
|
|
||||||
first == second && second == third
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_docstring {
|
|
||||||
if raw_text.contains(good_docstring("es_settings.docstring_quotes)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut diagnostic = Diagnostic::new(
|
|
||||||
violations::BadQuotesDocstring {
|
|
||||||
quote: quotes_settings.docstring_quotes.clone(),
|
|
||||||
},
|
|
||||||
Range::new(start, end),
|
|
||||||
);
|
|
||||||
if matches!(autofix, flags::Autofix::Enabled)
|
|
||||||
&& settings.rules.should_fix(&Rule::BadQuotesDocstring)
|
|
||||||
{
|
|
||||||
let quote_count = if is_multiline { 3 } else { 1 };
|
|
||||||
let string_contents = &raw_text[quote_count..raw_text.len() - quote_count];
|
|
||||||
let quote = good_docstring("es_settings.docstring_quotes).repeat(quote_count);
|
|
||||||
let mut fixed_contents =
|
|
||||||
String::with_capacity(prefix.len() + string_contents.len() + quote.len() * 2);
|
|
||||||
fixed_contents.push_str(prefix);
|
|
||||||
fixed_contents.push_str("e);
|
|
||||||
fixed_contents.push_str(string_contents);
|
|
||||||
fixed_contents.push_str("e);
|
|
||||||
diagnostic.amend(Fix::replacement(fixed_contents, start, end));
|
|
||||||
}
|
|
||||||
Some(diagnostic)
|
|
||||||
} else if is_multiline {
|
|
||||||
// If our string is or contains a known good string, ignore it.
|
|
||||||
if raw_text.contains(good_multiline("es_settings.multiline_quotes)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our string ends with a known good ending, then ignore it.
|
|
||||||
if raw_text.ends_with(good_multiline_ending("es_settings.multiline_quotes)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut diagnostic = Diagnostic::new(
|
|
||||||
violations::BadQuotesMultilineString {
|
|
||||||
quote: quotes_settings.multiline_quotes.clone(),
|
|
||||||
},
|
|
||||||
Range::new(start, end),
|
|
||||||
);
|
|
||||||
|
|
||||||
if matches!(autofix, flags::Autofix::Enabled)
|
|
||||||
&& settings.rules.should_fix(&Rule::BadQuotesMultilineString)
|
|
||||||
{
|
|
||||||
let string_contents = &raw_text[3..raw_text.len() - 3];
|
|
||||||
let quote = good_multiline("es_settings.multiline_quotes);
|
|
||||||
let mut fixed_contents =
|
|
||||||
String::with_capacity(prefix.len() + string_contents.len() + quote.len() * 2);
|
|
||||||
fixed_contents.push_str(prefix);
|
|
||||||
fixed_contents.push_str(quote);
|
|
||||||
fixed_contents.push_str(string_contents);
|
|
||||||
fixed_contents.push_str(quote);
|
|
||||||
diagnostic.amend(Fix::replacement(fixed_contents, start, end));
|
|
||||||
}
|
|
||||||
Some(diagnostic)
|
|
||||||
} else {
|
|
||||||
let string_contents = &raw_text[1..raw_text.len() - 1];
|
|
||||||
|
|
||||||
// If we're using the preferred quotation type, check for escapes.
|
|
||||||
if last_quote_char == good_single("es_settings.inline_quotes) {
|
|
||||||
if !quotes_settings.avoid_escape || prefix.contains('r') {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if string_contents.contains(good_single("es_settings.inline_quotes))
|
|
||||||
&& !string_contents.contains(bad_single("es_settings.inline_quotes))
|
|
||||||
{
|
|
||||||
let mut diagnostic =
|
|
||||||
Diagnostic::new(violations::AvoidQuoteEscape, Range::new(start, end));
|
|
||||||
if matches!(autofix, flags::Autofix::Enabled)
|
|
||||||
&& settings.rules.should_fix(&Rule::AvoidQuoteEscape)
|
|
||||||
{
|
|
||||||
let quote = bad_single("es_settings.inline_quotes);
|
|
||||||
|
|
||||||
let mut fixed_contents =
|
|
||||||
String::with_capacity(prefix.len() + string_contents.len() + 2);
|
|
||||||
fixed_contents.push_str(prefix);
|
|
||||||
fixed_contents.push(quote);
|
|
||||||
|
|
||||||
let chars: Vec<char> = string_contents.chars().collect();
|
|
||||||
let mut backslash_count = 0;
|
|
||||||
for col_offset in 0..chars.len() {
|
|
||||||
let char = chars[col_offset];
|
|
||||||
if char != '\\' {
|
|
||||||
fixed_contents.push(char);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
backslash_count += 1;
|
|
||||||
// If the previous character was also a backslash
|
|
||||||
if col_offset > 0 && chars[col_offset - 1] == '\\' && backslash_count == 2 {
|
|
||||||
fixed_contents.push(char);
|
|
||||||
// reset to 0
|
|
||||||
backslash_count = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If we're at the end of the line
|
|
||||||
if col_offset == chars.len() - 1 {
|
|
||||||
fixed_contents.push(char);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let next_char = chars[col_offset + 1];
|
|
||||||
// Remove quote escape
|
|
||||||
if next_char == '\'' || next_char == '"' {
|
|
||||||
// reset to 0
|
|
||||||
backslash_count = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fixed_contents.push(char);
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed_contents.push(quote);
|
|
||||||
|
|
||||||
diagnostic.amend(Fix::replacement(fixed_contents, start, end));
|
|
||||||
}
|
|
||||||
return Some(diagnostic);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not using the preferred type, only allow use to avoid escapes.
|
|
||||||
if !string_contents.contains(good_single("es_settings.inline_quotes)) {
|
|
||||||
let mut diagnostic = Diagnostic::new(
|
|
||||||
violations::BadQuotesInlineString {
|
|
||||||
quote: quotes_settings.inline_quotes.clone(),
|
|
||||||
},
|
|
||||||
Range::new(start, end),
|
|
||||||
);
|
|
||||||
if matches!(autofix, flags::Autofix::Enabled)
|
|
||||||
&& settings.rules.should_fix(&Rule::BadQuotesInlineString)
|
|
||||||
{
|
|
||||||
let quote = good_single("es_settings.inline_quotes);
|
|
||||||
let mut fixed_contents =
|
|
||||||
String::with_capacity(prefix.len() + string_contents.len() + 2);
|
|
||||||
fixed_contents.push_str(prefix);
|
|
||||||
fixed_contents.push(quote);
|
|
||||||
fixed_contents.push_str(string_contents);
|
|
||||||
fixed_contents.push(quote);
|
|
||||||
diagnostic.amend(Fix::replacement(fixed_contents, start, end));
|
|
||||||
}
|
|
||||||
return Some(diagnostic);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
violations::BadQuotesDocstring {
|
||||||
|
quote: quotes_settings.docstring_quotes.clone(),
|
||||||
|
},
|
||||||
|
Range::new(start, end),
|
||||||
|
);
|
||||||
|
if matches!(autofix, flags::Autofix::Enabled)
|
||||||
|
&& settings.rules.should_fix(&Rule::BadQuotesDocstring)
|
||||||
|
{
|
||||||
|
let quote_count = if trivia.is_multiline { 3 } else { 1 };
|
||||||
|
let string_contents = &trivia.raw_text[quote_count..trivia.raw_text.len() - quote_count];
|
||||||
|
let quote = good_docstring("es_settings.docstring_quotes).repeat(quote_count);
|
||||||
|
let mut fixed_contents =
|
||||||
|
String::with_capacity(trivia.prefix.len() + string_contents.len() + quote.len() * 2);
|
||||||
|
fixed_contents.push_str(trivia.prefix);
|
||||||
|
fixed_contents.push_str("e);
|
||||||
|
fixed_contents.push_str(string_contents);
|
||||||
|
fixed_contents.push_str("e);
|
||||||
|
diagnostic.amend(Fix::replacement(fixed_contents, start, end));
|
||||||
|
}
|
||||||
|
Some(diagnostic)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Q001, Q002
|
||||||
|
fn strings(
|
||||||
|
locator: &Locator,
|
||||||
|
sequence: &[(Location, Location)],
|
||||||
|
settings: &Settings,
|
||||||
|
autofix: flags::Autofix,
|
||||||
|
) -> Vec<Diagnostic> {
|
||||||
|
let mut diagnostics = vec![];
|
||||||
|
|
||||||
|
let quotes_settings = &settings.flake8_quotes;
|
||||||
|
|
||||||
|
let trivia = sequence
|
||||||
|
.iter()
|
||||||
|
.map(|(start, end)| {
|
||||||
|
let text = locator.slice_source_code_range(&Range::new(*start, *end));
|
||||||
|
let trivia: Trivia = text.into();
|
||||||
|
trivia
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Return `true` if any of the strings are inline strings that contain the quote character in
|
||||||
|
// the body.
|
||||||
|
let relax_quote = trivia.iter().any(|trivia| {
|
||||||
|
if trivia.is_multiline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if trivia.last_quote_char == good_single("es_settings.inline_quotes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let string_contents = &trivia.raw_text[1..trivia.raw_text.len() - 1];
|
||||||
|
string_contents.contains(good_single("es_settings.inline_quotes))
|
||||||
|
});
|
||||||
|
|
||||||
|
for ((start, end), trivia) in sequence.iter().zip(trivia.into_iter()) {
|
||||||
|
if trivia.is_multiline {
|
||||||
|
// If our string is or contains a known good string, ignore it.
|
||||||
|
if trivia
|
||||||
|
.raw_text
|
||||||
|
.contains(good_multiline("es_settings.multiline_quotes))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our string ends with a known good ending, then ignore it.
|
||||||
|
if trivia
|
||||||
|
.raw_text
|
||||||
|
.ends_with(good_multiline_ending("es_settings.multiline_quotes))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
violations::BadQuotesMultilineString {
|
||||||
|
quote: quotes_settings.multiline_quotes.clone(),
|
||||||
|
},
|
||||||
|
Range::new(*start, *end),
|
||||||
|
);
|
||||||
|
|
||||||
|
if matches!(autofix, flags::Autofix::Enabled)
|
||||||
|
&& settings.rules.should_fix(&Rule::BadQuotesMultilineString)
|
||||||
|
{
|
||||||
|
let string_contents = &trivia.raw_text[3..trivia.raw_text.len() - 3];
|
||||||
|
let quote = good_multiline("es_settings.multiline_quotes);
|
||||||
|
let mut fixed_contents = String::with_capacity(
|
||||||
|
trivia.prefix.len() + string_contents.len() + quote.len() * 2,
|
||||||
|
);
|
||||||
|
fixed_contents.push_str(trivia.prefix);
|
||||||
|
fixed_contents.push_str(quote);
|
||||||
|
fixed_contents.push_str(string_contents);
|
||||||
|
fixed_contents.push_str(quote);
|
||||||
|
diagnostic.amend(Fix::replacement(fixed_contents, *start, *end));
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
} else {
|
||||||
|
let string_contents = &trivia.raw_text[1..trivia.raw_text.len() - 1];
|
||||||
|
|
||||||
|
// If we're using the preferred quotation type, check for escapes.
|
||||||
|
if trivia.last_quote_char == good_single("es_settings.inline_quotes) {
|
||||||
|
if !quotes_settings.avoid_escape
|
||||||
|
|| trivia.prefix.contains('r')
|
||||||
|
|| trivia.prefix.contains('R')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if string_contents.contains(good_single("es_settings.inline_quotes))
|
||||||
|
&& !string_contents.contains(bad_single("es_settings.inline_quotes))
|
||||||
|
{
|
||||||
|
let mut diagnostic =
|
||||||
|
Diagnostic::new(violations::AvoidQuoteEscape, Range::new(*start, *end));
|
||||||
|
if matches!(autofix, flags::Autofix::Enabled)
|
||||||
|
&& settings.rules.should_fix(&Rule::AvoidQuoteEscape)
|
||||||
|
{
|
||||||
|
let quote = bad_single("es_settings.inline_quotes);
|
||||||
|
|
||||||
|
let mut fixed_contents =
|
||||||
|
String::with_capacity(trivia.prefix.len() + string_contents.len() + 2);
|
||||||
|
fixed_contents.push_str(trivia.prefix);
|
||||||
|
fixed_contents.push(quote);
|
||||||
|
|
||||||
|
let chars: Vec<char> = string_contents.chars().collect();
|
||||||
|
let mut backslash_count = 0;
|
||||||
|
for col_offset in 0..chars.len() {
|
||||||
|
let char = chars[col_offset];
|
||||||
|
if char != '\\' {
|
||||||
|
fixed_contents.push(char);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
backslash_count += 1;
|
||||||
|
// If the previous character was also a backslash
|
||||||
|
if col_offset > 0
|
||||||
|
&& chars[col_offset - 1] == '\\'
|
||||||
|
&& backslash_count == 2
|
||||||
|
{
|
||||||
|
fixed_contents.push(char);
|
||||||
|
// reset to 0
|
||||||
|
backslash_count = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If we're at the end of the line
|
||||||
|
if col_offset == chars.len() - 1 {
|
||||||
|
fixed_contents.push(char);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next_char = chars[col_offset + 1];
|
||||||
|
// Remove quote escape
|
||||||
|
if next_char == '\'' || next_char == '"' {
|
||||||
|
// reset to 0
|
||||||
|
backslash_count = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fixed_contents.push(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed_contents.push(quote);
|
||||||
|
|
||||||
|
diagnostic.amend(Fix::replacement(fixed_contents, *start, *end));
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not using the preferred type, only allow use to avoid escapes.
|
||||||
|
if !relax_quote {
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
violations::BadQuotesInlineString {
|
||||||
|
quote: quotes_settings.inline_quotes.clone(),
|
||||||
|
},
|
||||||
|
Range::new(*start, *end),
|
||||||
|
);
|
||||||
|
if matches!(autofix, flags::Autofix::Enabled)
|
||||||
|
&& settings.rules.should_fix(&Rule::BadQuotesInlineString)
|
||||||
|
{
|
||||||
|
let quote = good_single("es_settings.inline_quotes);
|
||||||
|
let mut fixed_contents =
|
||||||
|
String::with_capacity(trivia.prefix.len() + string_contents.len() + 2);
|
||||||
|
fixed_contents.push_str(trivia.prefix);
|
||||||
|
fixed_contents.push(quote);
|
||||||
|
fixed_contents.push_str(string_contents);
|
||||||
|
fixed_contents.push(quote);
|
||||||
|
diagnostic.amend(Fix::replacement(fixed_contents, *start, *end));
|
||||||
|
}
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate `flake8-quote` diagnostics from a token stream.
|
||||||
|
pub fn from_tokens(
|
||||||
|
lxr: &[LexResult],
|
||||||
|
locator: &Locator,
|
||||||
|
settings: &Settings,
|
||||||
|
autofix: flags::Autofix,
|
||||||
|
) -> Vec<Diagnostic> {
|
||||||
|
let mut diagnostics = vec![];
|
||||||
|
|
||||||
|
// Keep track of sequences of strings, which represent implicit string concatenation, and
|
||||||
|
// should thus be handled as a single unit.
|
||||||
|
let mut sequence = vec![];
|
||||||
|
let mut state_machine = StateMachine::default();
|
||||||
|
for &(start, ref tok, end) in lxr.iter().flatten() {
|
||||||
|
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, autofix));
|
||||||
|
sequence.clear();
|
||||||
|
}
|
||||||
|
if let Some(diagnostic) = docstring(locator, start, end, settings, autofix) {
|
||||||
|
diagnostics.push(diagnostic);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if matches!(tok, Tok::String { .. }) {
|
||||||
|
// If this is a string, add it to the sequence.
|
||||||
|
sequence.push((start, end));
|
||||||
|
} else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) {
|
||||||
|
// Otherwise, consume the sequence.
|
||||||
|
if !sequence.is_empty() {
|
||||||
|
diagnostics.extend(strings(locator, &sequence, settings, autofix));
|
||||||
|
sequence.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an unterminated sequence, consume it.
|
||||||
|
if !sequence.is_empty() {
|
||||||
|
diagnostics.extend(strings(locator, &sequence, settings, autofix));
|
||||||
|
sequence.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,23 @@ expression: diagnostics
|
||||||
row: 2
|
row: 2
|
||||||
column: 46
|
column: 46
|
||||||
parent: ~
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 24
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 46
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "f\"double quote string\""
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 24
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 46
|
||||||
|
parent: ~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,22 @@ expression: diagnostics
|
||||||
row: 1
|
row: 1
|
||||||
column: 47
|
column: 47
|
||||||
parent: ~
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
AvoidQuoteEscape: ~
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 16
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'\"string\"'"
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 16
|
||||||
|
parent: ~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
---
|
||||||
|
source: src/rules/flake8_quotes/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 2
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 2
|
||||||
|
column: 10
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"This\""
|
||||||
|
location:
|
||||||
|
row: 2
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 2
|
||||||
|
column: 10
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 8
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"is\""
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 8
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 9
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"not\""
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 9
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 10
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"This\""
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 10
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 8
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"is\""
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 8
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 9
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"not\""
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 9
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: double
|
||||||
|
location:
|
||||||
|
row: 27
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 27
|
||||||
|
column: 30
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"But this needs to be changed\""
|
||||||
|
location:
|
||||||
|
row: 27
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 27
|
||||||
|
column: 30
|
||||||
|
parent: ~
|
||||||
|
|
||||||
|
|
@ -40,4 +40,23 @@ expression: diagnostics
|
||||||
row: 2
|
row: 2
|
||||||
column: 46
|
column: 46
|
||||||
parent: ~
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 24
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 46
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "f'double quote string'"
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 24
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 46
|
||||||
|
parent: ~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,22 @@ expression: diagnostics
|
||||||
row: 2
|
row: 2
|
||||||
column: 52
|
column: 52
|
||||||
parent: ~
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
AvoidQuoteEscape: ~
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 16
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "\"'string'\""
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 16
|
||||||
|
parent: ~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
---
|
||||||
|
source: src/rules/flake8_quotes/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 2
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 2
|
||||||
|
column: 10
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'This'"
|
||||||
|
location:
|
||||||
|
row: 2
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 2
|
||||||
|
column: 10
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 8
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'is'"
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 3
|
||||||
|
column: 8
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 9
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'not'"
|
||||||
|
location:
|
||||||
|
row: 4
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 9
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 10
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'This'"
|
||||||
|
location:
|
||||||
|
row: 8
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 10
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 8
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'is'"
|
||||||
|
location:
|
||||||
|
row: 9
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 8
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 9
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'not'"
|
||||||
|
location:
|
||||||
|
row: 10
|
||||||
|
column: 4
|
||||||
|
end_location:
|
||||||
|
row: 10
|
||||||
|
column: 9
|
||||||
|
parent: ~
|
||||||
|
- kind:
|
||||||
|
BadQuotesInlineString:
|
||||||
|
quote: single
|
||||||
|
location:
|
||||||
|
row: 27
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 27
|
||||||
|
column: 30
|
||||||
|
fix:
|
||||||
|
content:
|
||||||
|
- "'But this needs to be changed'"
|
||||||
|
location:
|
||||||
|
row: 27
|
||||||
|
column: 0
|
||||||
|
end_location:
|
||||||
|
row: 27
|
||||||
|
column: 30
|
||||||
|
parent: ~
|
||||||
|
|
||||||
Loading…
Reference in New Issue