mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:26 +00:00
Add support for PEP 701 (#7376)
## Summary This PR adds support for PEP 701 in Ruff. This is a rollup PR of all the other individual PRs. The separate PRs were created for logic separation and code reviews. Refer to each pull request for a detail description on the change. Refer to the PR description for the list of pull requests within this PR. ## Test Plan ### Formatter ecosystem checks Explanation for the change in ecosystem check: https://github.com/astral-sh/ruff/pull/7597#issue-1908878183 #### `main` ``` | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76083 | 1789 | 1631 | | django | 0.99983 | 2760 | 36 | | transformers | 0.99963 | 2587 | 319 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99983 | 3496 | 18 | | warehouse | 0.99967 | 648 | 15 | | zulip | 0.99972 | 1437 | 21 | ``` #### `dhruv/pep-701` ``` | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76051 | 1789 | 1632 | | django | 0.99983 | 2760 | 36 | | transformers | 0.99963 | 2587 | 319 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99983 | 3496 | 18 | | warehouse | 0.99967 | 648 | 15 | | zulip | 0.99972 | 1437 | 21 | ```
This commit is contained in:
parent
78b8741352
commit
e62e245c61
115 changed files with 44780 additions and 31370 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2352,6 +2352,7 @@ name = "ruff_python_parser"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.0",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.11.0",
|
||||
|
|
|
@ -65,7 +65,7 @@ fn benchmark_formatter(criterion: &mut Criterion) {
|
|||
let comment_ranges = comment_ranges.finish();
|
||||
|
||||
// Parse the AST.
|
||||
let module = parse_tokens(tokens, Mode::Module, "<filename>")
|
||||
let module = parse_tokens(tokens, case.code(), Mode::Module, "<filename>")
|
||||
.expect("Input to be a valid python program");
|
||||
|
||||
b.iter(|| {
|
||||
|
|
|
@ -59,3 +59,23 @@ _ = "abc" + "def" + foo
|
|||
_ = foo + bar + "abc"
|
||||
_ = "abc" + foo + bar
|
||||
_ = foo + "abc" + bar
|
||||
|
||||
# Multiple strings nested inside a f-string
|
||||
_ = f"a {'b' 'c' 'd'} e"
|
||||
_ = f"""abc {"def" "ghi"} jkl"""
|
||||
_ = f"""abc {
|
||||
"def"
|
||||
"ghi"
|
||||
} jkl"""
|
||||
|
||||
# Nested f-strings
|
||||
_ = "a" f"b {f"c" f"d"} e" "f"
|
||||
_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
_ = f"b {f"abc" \
|
||||
f"def"} g"
|
||||
|
||||
# Explicitly concatenated nested f-strings
|
||||
_ = f"a {f"first"
|
||||
+ f"second"} d"
|
||||
_ = f"a {f"first {f"middle"}"
|
||||
+ f"second"} d"
|
||||
|
|
|
@ -9,3 +9,33 @@ this_should_raise = (
|
|||
'This is a'
|
||||
'\'string\''
|
||||
)
|
||||
|
||||
# Same as above, but with f-strings
|
||||
f'This is a \'string\'' # Q003
|
||||
f'This is \\ a \\\'string\'' # Q003
|
||||
f'"This" is a \'string\''
|
||||
f"This is a 'string'"
|
||||
f"\"This\" is a 'string'"
|
||||
fr'This is a \'string\''
|
||||
fR'This is a \'string\''
|
||||
foo = (
|
||||
f'This is a'
|
||||
f'\'string\'' # Q003
|
||||
)
|
||||
|
||||
# Nested f-strings (Python 3.12+)
|
||||
#
|
||||
# The first one is interesting because the fix for it is valid pre 3.12:
|
||||
#
|
||||
# f"'foo' {'nested'}"
|
||||
#
|
||||
# but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
f'\'foo\' {'nested'}' # Q003
|
||||
f'\'foo\' {f'nested'}' # Q003
|
||||
f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
|
||||
f'normal {f'nested'} normal'
|
||||
f'\'normal\' {f'nested'} normal' # Q003
|
||||
f'\'normal\' {f'nested'} "double quotes"'
|
||||
f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
|
|
|
@ -8,3 +8,32 @@ this_should_raise = (
|
|||
"This is a"
|
||||
"\"string\""
|
||||
)
|
||||
|
||||
# Same as above, but with f-strings
|
||||
f"This is a \"string\""
|
||||
f"'This' is a \"string\""
|
||||
f'This is a "string"'
|
||||
f'\'This\' is a "string"'
|
||||
fr"This is a \"string\""
|
||||
fR"This is a \"string\""
|
||||
foo = (
|
||||
f"This is a"
|
||||
f"\"string\""
|
||||
)
|
||||
|
||||
# Nested f-strings (Python 3.12+)
|
||||
#
|
||||
# The first one is interesting because the fix for it is valid pre 3.12:
|
||||
#
|
||||
# f'"foo" {"nested"}'
|
||||
#
|
||||
# but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
f"\"foo\" {"foo"}" # Q003
|
||||
f"\"foo\" {f"foo"}" # Q003
|
||||
f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
|
||||
f"normal {f"nested"} normal"
|
||||
f"\"normal\" {f"nested"} normal" # Q003
|
||||
f"\"normal\" {f"nested"} 'single quotes'"
|
||||
f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
|
|
|
@ -84,3 +84,8 @@ spam[ ~ham]
|
|||
x = [ #
|
||||
'some value',
|
||||
]
|
||||
|
||||
# F-strings
|
||||
f"{ {'a': 1} }"
|
||||
f"{[ { {'a': 1} } ]}"
|
||||
f"normal { {f"{ { [1, 2] } }" } } normal"
|
||||
|
|
|
@ -29,5 +29,16 @@ mdtypes_template = {
|
|||
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||
}
|
||||
|
||||
# E231
|
||||
f"{(a,b)}"
|
||||
|
||||
# Okay because it's hard to differentiate between the usages of a colon in a f-string
|
||||
f"{a:=1}"
|
||||
f"{ {'a':1} }"
|
||||
f"{a:.3f}"
|
||||
f"{(a:=1)}"
|
||||
f"{(lambda x:x)}"
|
||||
f"normal{f"{a:.3f}"}normal"
|
||||
|
||||
#: Okay
|
||||
a = (1,
|
||||
|
|
|
@ -48,3 +48,9 @@ def add(a: int=0, b: int =0, c: int= 0) -> int:
|
|||
#: Okay
|
||||
def add(a: int = _default(name='f')):
|
||||
return a
|
||||
|
||||
# F-strings
|
||||
f"{a=}"
|
||||
f"{a:=1}"
|
||||
f"{foo(a=1)}"
|
||||
f"normal {f"{a=}"} normal"
|
||||
|
|
|
@ -152,3 +152,11 @@ x = [
|
|||
multiline string with tab in it, different lines
|
||||
'''
|
||||
" single line string with tab in it"
|
||||
|
||||
f"test{
|
||||
tab_indented_should_be_flagged
|
||||
} <- this tab is fine"
|
||||
|
||||
f"""test{
|
||||
tab_indented_should_be_flagged
|
||||
} <- this tab is fine"""
|
||||
|
|
54
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_2.py
vendored
Normal file
54
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_2.py
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Same as `W605_0.py` but using f-strings instead.
|
||||
|
||||
#: W605:1:10
|
||||
regex = f'\.png$'
|
||||
|
||||
#: W605:2:1
|
||||
regex = f'''
|
||||
\.png$
|
||||
'''
|
||||
|
||||
#: W605:2:6
|
||||
f(
|
||||
f'\_'
|
||||
)
|
||||
|
||||
#: W605:4:6
|
||||
f"""
|
||||
multi-line
|
||||
literal
|
||||
with \_ somewhere
|
||||
in the middle
|
||||
"""
|
||||
|
||||
#: W605:1:38
|
||||
value = f'new line\nand invalid escape \_ here'
|
||||
|
||||
|
||||
#: Okay
|
||||
regex = fr'\.png$'
|
||||
regex = f'\\.png$'
|
||||
regex = fr'''
|
||||
\.png$
|
||||
'''
|
||||
regex = fr'''
|
||||
\\.png$
|
||||
'''
|
||||
s = f'\\'
|
||||
regex = f'\w' # noqa
|
||||
regex = f'''
|
||||
\w
|
||||
''' # noqa
|
||||
|
||||
regex = f'\\\_'
|
||||
value = f'\{{1}}'
|
||||
value = f'\{1}'
|
||||
value = f'{1:\}'
|
||||
value = f"{f"\{1}"}"
|
||||
value = rf"{f"\{1}"}"
|
||||
|
||||
# Okay
|
||||
value = rf'\{{1}}'
|
||||
value = rf'\{1}'
|
||||
value = rf'{1:\}'
|
||||
value = f"{rf"\{1}"}"
|
|
@ -40,7 +40,5 @@ f"{{{{x}}}}"
|
|||
""f""
|
||||
''f""
|
||||
(""f""r"")
|
||||
|
||||
# To be fixed
|
||||
# Error: f-string: single '}' is not allowed at line 41 column 8
|
||||
# f"\{{x}}"
|
||||
f"{v:{f"0.2f"}}"
|
||||
f"\{{x}}"
|
||||
|
|
Binary file not shown.
|
@ -29,3 +29,19 @@ x = "βα Bαd"
|
|||
# consisting of a single ambiguous character, while the second character is a "word
|
||||
# boundary" (whitespace) that it itself ambiguous.
|
||||
x = "Р усский"
|
||||
|
||||
# Same test cases as above but using f-strings instead:
|
||||
x = f"𝐁ad string"
|
||||
x = f"−"
|
||||
x = f"Русский"
|
||||
x = f"βα Bαd"
|
||||
x = f"Р усский"
|
||||
|
||||
# Nested f-strings
|
||||
x = f"𝐁ad string {f" {f"Р усский"}"}"
|
||||
|
||||
# Comments inside f-strings
|
||||
x = f"string { # And here's a comment with an unusual parenthesis: )
|
||||
# And here's a comment with a greek alpha: ∗
|
||||
foo # And here's a comment with an unusual punctuation mark: ᜵
|
||||
}"
|
||||
|
|
|
@ -966,9 +966,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
|||
pylint::rules::await_outside_async(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
Expr::FString(fstring @ ast::ExprFString { values, .. }) => {
|
||||
if checker.enabled(Rule::FStringMissingPlaceholders) {
|
||||
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
|
||||
pyflakes::rules::f_string_missing_placeholders(fstring, checker);
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedSQLExpression) {
|
||||
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);
|
||||
|
|
|
@ -183,7 +183,7 @@ impl<'a> Checker<'a> {
|
|||
|
||||
// Find the quote character used to start the containing f-string.
|
||||
let expr = self.semantic.current_expression()?;
|
||||
let string_range = self.indexer.f_string_range(expr.start())?;
|
||||
let string_range = self.indexer.fstring_ranges().innermost(expr.start())?;
|
||||
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
|
||||
|
||||
// Invert the quote character, if it's a single quote.
|
||||
|
|
|
@ -45,23 +45,25 @@ pub(crate) fn check_tokens(
|
|||
let mut state_machine = StateMachine::default();
|
||||
for &(ref tok, range) in tokens.iter().flatten() {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
||||
ruff::rules::ambiguous_unicode_character(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
range,
|
||||
if tok.is_string() {
|
||||
if is_docstring {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
}
|
||||
let context = match tok {
|
||||
Tok::String { .. } => {
|
||||
if is_docstring {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::Comment
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
Context::String
|
||||
}
|
||||
}
|
||||
Tok::FStringMiddle { .. } => Context::String,
|
||||
Tok::Comment(_) => Context::Comment,
|
||||
_ => continue,
|
||||
};
|
||||
ruff::rules::ambiguous_unicode_character(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
range,
|
||||
context,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,14 +77,14 @@ pub(crate) fn check_tokens(
|
|||
|
||||
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if tok.is_string() {
|
||||
pycodestyle::rules::invalid_escape_sequence(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
*range,
|
||||
settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
||||
);
|
||||
}
|
||||
pycodestyle::rules::invalid_escape_sequence(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
indexer,
|
||||
tok,
|
||||
*range,
|
||||
settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,9 +100,7 @@ pub(crate) fn check_tokens(
|
|||
Rule::InvalidCharacterZeroWidthSpace,
|
||||
]) {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if tok.is_string() {
|
||||
pylint::rules::invalid_string_characters(&mut diagnostics, *range, locator);
|
||||
}
|
||||
pylint::rules::invalid_string_characters(&mut diagnostics, tok, *range, locator);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,13 +118,16 @@ pub(crate) fn check_tokens(
|
|||
);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::AvoidableEscapedQuote) && settings.flake8_quotes.avoid_escape {
|
||||
flake8_quotes::rules::avoidable_escaped_quote(&mut diagnostics, tokens, locator, settings);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::BadQuotesInlineString,
|
||||
Rule::BadQuotesMultilineString,
|
||||
Rule::BadQuotesDocstring,
|
||||
Rule::AvoidableEscapedQuote,
|
||||
]) {
|
||||
flake8_quotes::rules::from_tokens(&mut diagnostics, tokens, locator, settings);
|
||||
flake8_quotes::rules::check_string_quotes(&mut diagnostics, tokens, locator, settings);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
|
@ -136,6 +139,7 @@ pub(crate) fn check_tokens(
|
|||
tokens,
|
||||
&settings.flake8_implicit_str_concat,
|
||||
locator,
|
||||
indexer,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Extract `# noqa`, `# isort: skip`, and `# TODO` directives from tokenized source.
|
||||
|
||||
use std::iter::Peekable;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
@ -85,6 +86,39 @@ pub fn extract_directives(
|
|||
}
|
||||
}
|
||||
|
||||
struct SortedMergeIter<L, R, Item>
|
||||
where
|
||||
L: Iterator<Item = Item>,
|
||||
R: Iterator<Item = Item>,
|
||||
{
|
||||
left: Peekable<L>,
|
||||
right: Peekable<R>,
|
||||
}
|
||||
|
||||
impl<L, R, Item> Iterator for SortedMergeIter<L, R, Item>
|
||||
where
|
||||
L: Iterator<Item = Item>,
|
||||
R: Iterator<Item = Item>,
|
||||
Item: Ranged,
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.left.peek(), self.right.peek()) {
|
||||
(Some(left), Some(right)) => {
|
||||
if left.start() <= right.start() {
|
||||
Some(self.left.next().unwrap())
|
||||
} else {
|
||||
Some(self.right.next().unwrap())
|
||||
}
|
||||
}
|
||||
(Some(_), None) => Some(self.left.next().unwrap()),
|
||||
(None, Some(_)) => Some(self.right.next().unwrap()),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a mapping from logical line to noqa line.
|
||||
fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer) -> NoqaMapping {
|
||||
let mut string_mappings = Vec::new();
|
||||
|
@ -113,6 +147,29 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
|
|||
}
|
||||
}
|
||||
|
||||
// The capacity allocated here might be more than we need if there are
|
||||
// nested f-strings.
|
||||
let mut fstring_mappings = Vec::with_capacity(indexer.fstring_ranges().len());
|
||||
|
||||
// For nested f-strings, we expect `noqa` directives on the last line of the
|
||||
// outermost f-string. The last f-string range will be used to skip over
|
||||
// the inner f-strings.
|
||||
let mut last_fstring_range: TextRange = TextRange::default();
|
||||
for fstring_range in indexer.fstring_ranges().values() {
|
||||
if !locator.contains_line_break(*fstring_range) {
|
||||
continue;
|
||||
}
|
||||
if last_fstring_range.contains_range(*fstring_range) {
|
||||
continue;
|
||||
}
|
||||
let new_range = TextRange::new(
|
||||
locator.line_start(fstring_range.start()),
|
||||
fstring_range.end(),
|
||||
);
|
||||
fstring_mappings.push(new_range);
|
||||
last_fstring_range = new_range;
|
||||
}
|
||||
|
||||
let mut continuation_mappings = Vec::new();
|
||||
|
||||
// For continuations, we expect `noqa` directives on the last line of the
|
||||
|
@ -137,27 +194,20 @@ fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer
|
|||
}
|
||||
|
||||
// Merge the mappings in sorted order
|
||||
let mut mappings =
|
||||
NoqaMapping::with_capacity(continuation_mappings.len() + string_mappings.len());
|
||||
let mut mappings = NoqaMapping::with_capacity(
|
||||
continuation_mappings.len() + string_mappings.len() + fstring_mappings.len(),
|
||||
);
|
||||
|
||||
let mut continuation_mappings = continuation_mappings.into_iter().peekable();
|
||||
let mut string_mappings = string_mappings.into_iter().peekable();
|
||||
let string_mappings = SortedMergeIter {
|
||||
left: fstring_mappings.into_iter().peekable(),
|
||||
right: string_mappings.into_iter().peekable(),
|
||||
};
|
||||
let all_mappings = SortedMergeIter {
|
||||
left: string_mappings.peekable(),
|
||||
right: continuation_mappings.into_iter().peekable(),
|
||||
};
|
||||
|
||||
while let (Some(continuation), Some(string)) =
|
||||
(continuation_mappings.peek(), string_mappings.peek())
|
||||
{
|
||||
if continuation.start() <= string.start() {
|
||||
mappings.push_mapping(continuation_mappings.next().unwrap());
|
||||
} else {
|
||||
mappings.push_mapping(string_mappings.next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
for mapping in continuation_mappings {
|
||||
mappings.push_mapping(mapping);
|
||||
}
|
||||
|
||||
for mapping in string_mappings {
|
||||
for mapping in all_mappings {
|
||||
mappings.push_mapping(mapping);
|
||||
}
|
||||
|
||||
|
@ -429,6 +479,67 @@ ghi
|
|||
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(28))])
|
||||
);
|
||||
|
||||
let contents = "x = f'abc {
|
||||
a
|
||||
*
|
||||
b
|
||||
}'
|
||||
y = 2
|
||||
";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(32))])
|
||||
);
|
||||
|
||||
let contents = "x = f'''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
y = 2
|
||||
z = x + 1";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(23))])
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = f'''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
z = 2";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(29))])
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = f'''abc
|
||||
def
|
||||
ghi
|
||||
'''";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(29))])
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = f'''abc
|
||||
def {f'''nested
|
||||
fstring''' f'another nested'}
|
||||
end'''
|
||||
";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(70))])
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = f'normal'
|
||||
z = f'another but {f'nested but {f'still single line'} nested'}'
|
||||
";
|
||||
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
|
||||
|
||||
let contents = r"x = \
|
||||
1";
|
||||
assert_eq!(
|
||||
|
|
|
@ -143,6 +143,7 @@ pub fn check_path(
|
|||
if use_ast || use_imports || use_doc_lines {
|
||||
match ruff_python_parser::parse_program_tokens(
|
||||
tokens,
|
||||
source_kind.source_code(),
|
||||
&path.to_string_lossy(),
|
||||
source_type.is_ipynb(),
|
||||
) {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use itertools::Itertools;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::rules::flake8_implicit_str_concat::settings::Settings;
|
||||
|
@ -94,6 +96,7 @@ pub(crate) fn implicit(
|
|||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) {
|
||||
for ((a_tok, a_range), (b_tok, b_range)) in tokens
|
||||
.iter()
|
||||
|
@ -103,24 +106,39 @@ pub(crate) fn implicit(
|
|||
})
|
||||
.tuple_windows()
|
||||
{
|
||||
if a_tok.is_string() && b_tok.is_string() {
|
||||
if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
MultiLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
));
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SingleLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
);
|
||||
let (a_range, b_range) = match (a_tok, b_tok) {
|
||||
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
|
||||
(Tok::String { .. }, Tok::FStringStart) => (
|
||||
*a_range,
|
||||
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
|
||||
),
|
||||
(Tok::FStringEnd, Tok::String { .. }) => (
|
||||
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
|
||||
*b_range,
|
||||
),
|
||||
(Tok::FStringEnd, Tok::FStringStart) => (
|
||||
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
|
||||
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
|
||||
),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if let Some(fix) = concatenate_strings(*a_range, *b_range, locator) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
MultiLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
));
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SingleLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
);
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
};
|
||||
if let Some(fix) = concatenate_strings(a_range, b_range, locator) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,4 +153,147 @@ ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line
|
|||
54 54 | # Single-line explicit concatenation should be ignored.
|
||||
55 55 | _ = "abc" + "def" + "ghi"
|
||||
|
||||
ISC.py:64:10: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
| ^^^^^^^ ISC001
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 | _ = f"""abc {
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
61 61 | _ = foo + "abc" + bar
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 |-_ = f"a {'b' 'c' 'd'} e"
|
||||
64 |+_ = f"a {'bc' 'd'} e"
|
||||
65 65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
|
||||
ISC.py:64:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
| ^^^^^^^ ISC001
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 | _ = f"""abc {
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
61 61 | _ = foo + "abc" + bar
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 |-_ = f"a {'b' 'c' 'd'} e"
|
||||
64 |+_ = f"a {'b' 'cd'} e"
|
||||
65 65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
|
||||
ISC.py:65:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
| ^^^^^^^^^^^ ISC001
|
||||
66 | _ = f"""abc {
|
||||
67 | "def"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
65 |-_ = f"""abc {"def" "ghi"} jkl"""
|
||||
65 |+_ = f"""abc {"defghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
68 68 | "ghi"
|
||||
|
||||
ISC.py:72:5: ISC001 Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ISC.py:72:9: ISC001 Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ISC.py:72:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
69 69 | } jkl"""
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 |-_ = "a" f"b {f"c" f"d"} e" "f"
|
||||
72 |+_ = "a" f"b {f"cd"} e" "f"
|
||||
73 73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
|
||||
ISC.py:73:10: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
74 | _ = f"b {f"abc" \
|
||||
75 | f"def"} g"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 |-_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
73 |+_ = f"b {f"cd {f"e" f"f"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
ISC.py:73:20: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
| ^^^^^^^^^ ISC001
|
||||
74 | _ = f"b {f"abc" \
|
||||
75 | f"def"} g"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 |-_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
73 |+_ = f"b {f"c" f"d {f"ef"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
|
||||
|
|
|
@ -13,4 +13,16 @@ ISC.py:5:5: ISC002 Implicitly concatenated string literals over multiple lines
|
|||
8 | _ = (
|
||||
|
|
||||
|
||||
ISC.py:74:10: ISC002 Implicitly concatenated string literals over multiple lines
|
||||
|
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
| __________^
|
||||
75 | | f"def"} g"
|
||||
| |__________^ ISC002
|
||||
76 |
|
||||
77 | # Explicitly concatenated nested f-strings
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -31,4 +31,25 @@ ISC.py:19:3: ISC003 Explicitly concatenated string should be implicitly concaten
|
|||
21 | )
|
||||
|
|
||||
|
||||
ISC.py:78:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
77 | # Explicitly concatenated nested f-strings
|
||||
78 | _ = f"a {f"first"
|
||||
| __________^
|
||||
79 | | + f"second"} d"
|
||||
| |_______________^ ISC003
|
||||
80 | _ = f"a {f"first {f"middle"}"
|
||||
81 | + f"second"} d"
|
||||
|
|
||||
|
||||
ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
78 | _ = f"a {f"first"
|
||||
79 | + f"second"} d"
|
||||
80 | _ = f"a {f"first {f"middle"}"
|
||||
| __________^
|
||||
81 | | + f"second"} d"
|
||||
| |_______________^ ISC003
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -153,4 +153,147 @@ ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line
|
|||
54 54 | # Single-line explicit concatenation should be ignored.
|
||||
55 55 | _ = "abc" + "def" + "ghi"
|
||||
|
||||
ISC.py:64:10: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
| ^^^^^^^ ISC001
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 | _ = f"""abc {
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
61 61 | _ = foo + "abc" + bar
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 |-_ = f"a {'b' 'c' 'd'} e"
|
||||
64 |+_ = f"a {'bc' 'd'} e"
|
||||
65 65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
|
||||
ISC.py:64:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
| ^^^^^^^ ISC001
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 | _ = f"""abc {
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
61 61 | _ = foo + "abc" + bar
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 |-_ = f"a {'b' 'c' 'd'} e"
|
||||
64 |+_ = f"a {'b' 'cd'} e"
|
||||
65 65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
|
||||
ISC.py:65:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
63 | # Multiple strings nested inside a f-string
|
||||
64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
| ^^^^^^^^^^^ ISC001
|
||||
66 | _ = f"""abc {
|
||||
67 | "def"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
62 62 |
|
||||
63 63 | # Multiple strings nested inside a f-string
|
||||
64 64 | _ = f"a {'b' 'c' 'd'} e"
|
||||
65 |-_ = f"""abc {"def" "ghi"} jkl"""
|
||||
65 |+_ = f"""abc {"defghi"} jkl"""
|
||||
66 66 | _ = f"""abc {
|
||||
67 67 | "def"
|
||||
68 68 | "ghi"
|
||||
|
||||
ISC.py:72:5: ISC001 Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ISC.py:72:9: ISC001 Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ISC.py:72:14: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
| ^^^^^^^^^ ISC001
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
69 69 | } jkl"""
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 |-_ = "a" f"b {f"c" f"d"} e" "f"
|
||||
72 |+_ = "a" f"b {f"cd"} e" "f"
|
||||
73 73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
|
||||
ISC.py:73:10: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ ISC001
|
||||
74 | _ = f"b {f"abc" \
|
||||
75 | f"def"} g"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 |-_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
73 |+_ = f"b {f"cd {f"e" f"f"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
ISC.py:73:20: ISC001 [*] Implicitly concatenated string literals on one line
|
||||
|
|
||||
71 | # Nested f-strings
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
| ^^^^^^^^^ ISC001
|
||||
74 | _ = f"b {f"abc" \
|
||||
75 | f"def"} g"
|
||||
|
|
||||
= help: Combine string literals
|
||||
|
||||
ℹ Fix
|
||||
70 70 |
|
||||
71 71 | # Nested f-strings
|
||||
72 72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 |-_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
73 |+_ = f"b {f"c" f"d {f"ef"} g"} h"
|
||||
74 74 | _ = f"b {f"abc" \
|
||||
75 75 | f"def"} g"
|
||||
76 76 |
|
||||
|
||||
|
||||
|
|
|
@ -43,4 +43,27 @@ ISC.py:34:3: ISC002 Implicitly concatenated string literals over multiple lines
|
|||
36 | )
|
||||
|
|
||||
|
||||
ISC.py:67:5: ISC002 Implicitly concatenated string literals over multiple lines
|
||||
|
|
||||
65 | _ = f"""abc {"def" "ghi"} jkl"""
|
||||
66 | _ = f"""abc {
|
||||
67 | "def"
|
||||
| _____^
|
||||
68 | | "ghi"
|
||||
| |_________^ ISC002
|
||||
69 | } jkl"""
|
||||
|
|
||||
|
||||
ISC.py:74:10: ISC002 Implicitly concatenated string literals over multiple lines
|
||||
|
|
||||
72 | _ = "a" f"b {f"c" f"d"} e" "f"
|
||||
73 | _ = f"b {f"c" f"d {f"e" f"f"} g"} h"
|
||||
74 | _ = f"b {f"abc" \
|
||||
| __________^
|
||||
75 | | f"def"} g"
|
||||
| |__________^ ISC002
|
||||
76 |
|
||||
77 | # Explicitly concatenated nested f-strings
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -31,4 +31,25 @@ ISC.py:19:3: ISC003 Explicitly concatenated string should be implicitly concaten
|
|||
21 | )
|
||||
|
|
||||
|
||||
ISC.py:78:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
77 | # Explicitly concatenated nested f-strings
|
||||
78 | _ = f"a {f"first"
|
||||
| __________^
|
||||
79 | | + f"second"} d"
|
||||
| |_______________^ ISC003
|
||||
80 | _ = f"a {f"first {f"middle"}"
|
||||
81 | + f"second"} d"
|
||||
|
|
||||
|
||||
ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
|
|
||||
78 | _ = f"a {f"first"
|
||||
79 | + f"second"} d"
|
||||
80 | _ = f"a {f"first {f"middle"}"
|
||||
| __________^
|
||||
81 | | + f"second"} d"
|
||||
| |_______________^ ISC003
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ mod tests {
|
|||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
|
||||
|
@ -45,6 +46,44 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_singles_over_doubles_escaped_py311() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_quotes/doubles_escaped.py"),
|
||||
&LinterSettings {
|
||||
flake8_quotes: super::settings::Settings {
|
||||
inline_quotes: Quote::Single,
|
||||
multiline_quotes: Quote::Single,
|
||||
docstring_quotes: Quote::Single,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::AvoidableEscapedQuote)
|
||||
.with_target_version(PythonVersion::Py311)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_doubles_over_singles_escaped_py311() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_quotes/singles_escaped.py"),
|
||||
&LinterSettings {
|
||||
flake8_quotes: super::settings::Settings {
|
||||
inline_quotes: Quote::Double,
|
||||
multiline_quotes: Quote::Double,
|
||||
docstring_quotes: Quote::Double,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::AvoidableEscapedQuote)
|
||||
.with_target_version(PythonVersion::Py311)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("singles.py"))]
|
||||
#[test_case(Path::new("singles_escaped.py"))]
|
||||
#[test_case(Path::new("singles_implicit.py"))]
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::str::{is_triple_quote, leading_quote};
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::AsRule;
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for strings that include escaped quotes, and suggests changing
|
||||
/// the quote style to avoid the need to escape them.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It's preferable to avoid escaped quotes in strings. By changing the
|
||||
/// outer quote style, you can avoid escaping inner quotes.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo = 'bar\'s'
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo = "bar's"
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AvoidableEscapedQuote;
|
||||
|
||||
impl AlwaysFixableViolation for AvoidableEscapedQuote {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Change outer quotes to avoid escaping inner quotes")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Change outer quotes to avoid escaping inner quotes".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct FStringContext {
|
||||
/// Whether to check for escaped quotes in the f-string.
|
||||
check_for_escaped_quote: bool,
|
||||
/// The range of the f-string start token.
|
||||
start_range: TextRange,
|
||||
/// The ranges of the f-string middle tokens containing escaped quotes.
|
||||
middle_ranges_with_escapes: Vec<TextRange>,
|
||||
}
|
||||
|
||||
impl FStringContext {
|
||||
fn new(check_for_escaped_quote: bool, fstring_start_range: TextRange) -> Self {
|
||||
Self {
|
||||
check_for_escaped_quote,
|
||||
start_range: fstring_start_range,
|
||||
middle_ranges_with_escapes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the context to not check for escaped quotes, and clear any
|
||||
/// existing reported ranges.
|
||||
fn ignore_escaped_quotes(&mut self) {
|
||||
self.check_for_escaped_quote = false;
|
||||
self.middle_ranges_with_escapes.clear();
|
||||
}
|
||||
|
||||
fn push_fstring_middle_range(&mut self, range: TextRange) {
|
||||
self.middle_ranges_with_escapes.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
/// Q003
|
||||
pub(crate) fn avoidable_escaped_quote(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
lxr: &[LexResult],
|
||||
locator: &Locator,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let quotes_settings = &settings.flake8_quotes;
|
||||
let supports_pep701 = settings.target_version.supports_pep701();
|
||||
let mut fstrings: Vec<FStringContext> = Vec::new();
|
||||
let mut state_machine = StateMachine::default();
|
||||
|
||||
for &(ref tok, tok_range) in lxr.iter().flatten() {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
if is_docstring {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !supports_pep701 {
|
||||
// If this is a string or a start of a f-string which is inside another
|
||||
// f-string, we won't check for escaped quotes for the entire f-string
|
||||
// if the target version doesn't support PEP 701. For example:
|
||||
//
|
||||
// ```python
|
||||
// f"\"foo\" {'nested'}"
|
||||
// # ^^^^^^^^
|
||||
// # We're here
|
||||
// ```
|
||||
//
|
||||
// If we try to fix the above example, the outer and inner quote
|
||||
// will be the same which is invalid pre 3.12:
|
||||
//
|
||||
// ```python
|
||||
// f'"foo" {'nested'}"
|
||||
// ```
|
||||
if matches!(tok, Tok::String { .. } | Tok::FStringStart) {
|
||||
if let Some(fstring_context) = fstrings.last_mut() {
|
||||
fstring_context.ignore_escaped_quotes();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tok {
|
||||
Tok::String {
|
||||
value: string_contents,
|
||||
kind,
|
||||
triple_quoted,
|
||||
} => {
|
||||
if kind.is_raw() || *triple_quoted {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we're using the preferred quotation style.
|
||||
if !leading_quote(locator.slice(tok_range))
|
||||
.is_some_and(|text| text.contains(quotes_settings.inline_quotes.as_char()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if string_contents.contains(quotes_settings.inline_quotes.as_char())
|
||||
&& !string_contents.contains(quotes_settings.inline_quotes.opposite().as_char())
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, tok_range);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
let fixed_contents = format!(
|
||||
"{prefix}{quote}{value}{quote}",
|
||||
prefix = kind.as_str(),
|
||||
quote = quotes_settings.inline_quotes.opposite().as_char(),
|
||||
value = unescape_string(string_contents)
|
||||
);
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
fixed_contents,
|
||||
tok_range,
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Tok::FStringStart => {
|
||||
let text = locator.slice(tok_range);
|
||||
// Check for escaped quote only if we're using the preferred quotation
|
||||
// style and it isn't a triple-quoted f-string.
|
||||
let check_for_escaped_quote = text
|
||||
.contains(quotes_settings.inline_quotes.as_char())
|
||||
&& !is_triple_quote(text);
|
||||
fstrings.push(FStringContext::new(check_for_escaped_quote, tok_range));
|
||||
}
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
} if !is_raw => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
if !context.check_for_escaped_quote {
|
||||
continue;
|
||||
}
|
||||
// If any part of the f-string contains the opposite quote,
|
||||
// we can't change the quote style in the entire f-string.
|
||||
if string_contents.contains(quotes_settings.inline_quotes.opposite().as_char()) {
|
||||
context.ignore_escaped_quotes();
|
||||
continue;
|
||||
}
|
||||
if string_contents.contains(quotes_settings.inline_quotes.as_char()) {
|
||||
context.push_fstring_middle_range(tok_range);
|
||||
}
|
||||
}
|
||||
Tok::FStringEnd => {
|
||||
let Some(context) = fstrings.pop() else {
|
||||
continue;
|
||||
};
|
||||
if context.middle_ranges_with_escapes.is_empty() {
|
||||
// There are no `FStringMiddle` tokens containing any escaped
|
||||
// quotes.
|
||||
continue;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
AvoidableEscapedQuote,
|
||||
TextRange::new(context.start_range.start(), tok_range.end()),
|
||||
);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
let fstring_start_edit = Edit::range_replacement(
|
||||
// No need for `r`/`R` as we don't perform the checks
|
||||
// for raw strings.
|
||||
format!("f{}", quotes_settings.inline_quotes.opposite().as_char()),
|
||||
context.start_range,
|
||||
);
|
||||
let fstring_middle_and_end_edits = context
|
||||
.middle_ranges_with_escapes
|
||||
.iter()
|
||||
.map(|&range| {
|
||||
Edit::range_replacement(unescape_string(locator.slice(range)), range)
|
||||
})
|
||||
.chain(std::iter::once(
|
||||
// `FStringEnd` edit
|
||||
Edit::range_replacement(
|
||||
quotes_settings
|
||||
.inline_quotes
|
||||
.opposite()
|
||||
.as_char()
|
||||
.to_string(),
|
||||
tok_range,
|
||||
),
|
||||
));
|
||||
diagnostic.set_fix(Fix::automatic_edits(
|
||||
fstring_start_edit,
|
||||
fstring_middle_and_end_edits,
|
||||
));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unescape_string(value: &str) -> String {
|
||||
let mut fixed_contents = String::with_capacity(value.len());
|
||||
|
||||
let mut chars = value.chars().peekable();
|
||||
while let Some(char) = chars.next() {
|
||||
if char != '\\' {
|
||||
fixed_contents.push(char);
|
||||
continue;
|
||||
}
|
||||
// If we're at the end of the line
|
||||
let Some(next_char) = chars.peek() else {
|
||||
fixed_contents.push(char);
|
||||
continue;
|
||||
};
|
||||
// Remove quote escape
|
||||
if matches!(*next_char, '\'' | '"') {
|
||||
continue;
|
||||
}
|
||||
fixed_contents.push(char);
|
||||
}
|
||||
|
||||
fixed_contents
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
@ -34,22 +34,22 @@ use super::super::settings::Quote;
|
|||
/// - `flake8-quotes.inline-quotes`
|
||||
#[violation]
|
||||
pub struct BadQuotesInlineString {
|
||||
quote: Quote,
|
||||
preferred_quote: Quote,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for BadQuotesInlineString {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadQuotesInlineString { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesInlineString { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => format!("Single quotes found but double quotes preferred"),
|
||||
Quote::Single => format!("Double quotes found but single quotes preferred"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let BadQuotesInlineString { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesInlineString { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => "Replace single quotes with double quotes".to_string(),
|
||||
Quote::Single => "Replace double quotes with single quotes".to_string(),
|
||||
}
|
||||
|
@ -83,22 +83,22 @@ impl AlwaysFixableViolation for BadQuotesInlineString {
|
|||
/// - `flake8-quotes.multiline-quotes`
|
||||
#[violation]
|
||||
pub struct BadQuotesMultilineString {
|
||||
quote: Quote,
|
||||
preferred_quote: Quote,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for BadQuotesMultilineString {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadQuotesMultilineString { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesMultilineString { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => format!("Single quote multiline found but double quotes preferred"),
|
||||
Quote::Single => format!("Double quote multiline found but single quotes preferred"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let BadQuotesMultilineString { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesMultilineString { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => "Replace single multiline quotes with double quotes".to_string(),
|
||||
Quote::Single => "Replace double multiline quotes with single quotes".to_string(),
|
||||
}
|
||||
|
@ -131,73 +131,28 @@ impl AlwaysFixableViolation for BadQuotesMultilineString {
|
|||
/// - `flake8-quotes.docstring-quotes`
|
||||
#[violation]
|
||||
pub struct BadQuotesDocstring {
|
||||
quote: Quote,
|
||||
preferred_quote: Quote,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for BadQuotesDocstring {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadQuotesDocstring { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesDocstring { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => format!("Single quote docstring found but double quotes preferred"),
|
||||
Quote::Single => format!("Double quote docstring found but single quotes preferred"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let BadQuotesDocstring { quote } = self;
|
||||
match quote {
|
||||
let BadQuotesDocstring { preferred_quote } = self;
|
||||
match preferred_quote {
|
||||
Quote::Double => "Replace single quotes docstring with double quotes".to_string(),
|
||||
Quote::Single => "Replace double quotes docstring with single quotes".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for strings that include escaped quotes, and suggests changing
|
||||
/// the quote style to avoid the need to escape them.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It's preferable to avoid escaped quotes in strings. By changing the
|
||||
/// outer quote style, you can avoid escaping inner quotes.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo = 'bar\'s'
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo = "bar's"
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AvoidableEscapedQuote;
|
||||
|
||||
impl AlwaysFixableViolation for AvoidableEscapedQuote {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Change outer quotes to avoid escaping inner quotes")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Change outer quotes to avoid escaping inner quotes".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
const fn good_single(quote: Quote) -> char {
|
||||
match quote {
|
||||
Quote::Double => '"',
|
||||
Quote::Single => '\'',
|
||||
}
|
||||
}
|
||||
|
||||
const fn bad_single(quote: Quote) -> char {
|
||||
match quote {
|
||||
Quote::Double => '\'',
|
||||
Quote::Single => '"',
|
||||
}
|
||||
}
|
||||
|
||||
const fn good_multiline(quote: Quote) -> &'static str {
|
||||
match quote {
|
||||
Quote::Double => "\"\"\"",
|
||||
|
@ -219,6 +174,7 @@ const fn good_docstring(quote: Quote) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Trivia<'a> {
|
||||
last_quote_char: char,
|
||||
prefix: &'a str,
|
||||
|
@ -254,7 +210,7 @@ impl<'a> From<&'a str> for Trivia<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Q003
|
||||
/// Q002
|
||||
fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> Option<Diagnostic> {
|
||||
let quotes_settings = &settings.flake8_quotes;
|
||||
|
||||
|
@ -270,7 +226,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) ->
|
|||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BadQuotesDocstring {
|
||||
quote: quotes_settings.docstring_quotes,
|
||||
preferred_quote: quotes_settings.docstring_quotes,
|
||||
},
|
||||
range,
|
||||
);
|
||||
|
@ -292,7 +248,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) ->
|
|||
Some(diagnostic)
|
||||
}
|
||||
|
||||
/// Q001, Q002
|
||||
/// Q000, Q001
|
||||
fn strings(
|
||||
locator: &Locator,
|
||||
sequence: &[TextRange],
|
||||
|
@ -318,12 +274,12 @@ fn strings(
|
|||
return false;
|
||||
}
|
||||
|
||||
if trivia.last_quote_char == good_single(quotes_settings.inline_quotes) {
|
||||
if trivia.last_quote_char == quotes_settings.inline_quotes.as_char() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let string_contents = &trivia.raw_text[1..trivia.raw_text.len() - 1];
|
||||
string_contents.contains(good_single(quotes_settings.inline_quotes))
|
||||
string_contents.contains(quotes_settings.inline_quotes.as_char())
|
||||
});
|
||||
|
||||
for (range, trivia) in sequence.iter().zip(trivia) {
|
||||
|
@ -346,7 +302,7 @@ fn strings(
|
|||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BadQuotesMultilineString {
|
||||
quote: quotes_settings.multiline_quotes,
|
||||
preferred_quote: quotes_settings.multiline_quotes,
|
||||
},
|
||||
*range,
|
||||
);
|
||||
|
@ -367,107 +323,90 @@ fn strings(
|
|||
)));
|
||||
}
|
||||
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(quotes_settings.inline_quotes) {
|
||||
if !quotes_settings.avoid_escape
|
||||
|| trivia.prefix.contains('r')
|
||||
|| trivia.prefix.contains('R')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if string_contents.contains(good_single(quotes_settings.inline_quotes))
|
||||
&& !string_contents.contains(bad_single(quotes_settings.inline_quotes))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, *range);
|
||||
if settings.rules.should_fix(Rule::AvoidableEscapedQuote) {
|
||||
let quote = bad_single(quotes_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.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
fixed_contents,
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
} 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 !relax_quote {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BadQuotesInlineString {
|
||||
quote: quotes_settings.inline_quotes,
|
||||
},
|
||||
&& !relax_quote
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
BadQuotesInlineString {
|
||||
preferred_quote: quotes_settings.inline_quotes,
|
||||
},
|
||||
*range,
|
||||
);
|
||||
if settings.rules.should_fix(Rule::BadQuotesInlineString) {
|
||||
let quote = quotes_settings.inline_quotes.as_char();
|
||||
let string_contents = &trivia.raw_text[1..trivia.raw_text.len() - 1];
|
||||
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.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
fixed_contents,
|
||||
*range,
|
||||
);
|
||||
if settings.rules.should_fix(Rule::BadQuotesInlineString) {
|
||||
let quote = good_single(quotes_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.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
fixed_contents,
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
)));
|
||||
}
|
||||
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.
|
||||
pub(crate) fn from_tokens(
|
||||
pub(crate) fn check_string_quotes(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
lxr: &[LexResult],
|
||||
locator: &Locator,
|
||||
|
@ -477,7 +416,13 @@ pub(crate) fn from_tokens(
|
|||
// concatenation, and should thus be handled as a single unit.
|
||||
let mut sequence = vec![];
|
||||
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
|
||||
|
@ -491,14 +436,23 @@ pub(crate) fn from_tokens(
|
|||
diagnostics.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
if tok.is_string() {
|
||||
// If this is a string, add it to the sequence.
|
||||
sequence.push(range);
|
||||
} else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) {
|
||||
// Otherwise, consume the sequence.
|
||||
if !sequence.is_empty() {
|
||||
diagnostics.extend(strings(locator, &sequence, settings));
|
||||
sequence.clear();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
pub(crate) use from_tokens::*;
|
||||
pub(crate) use avoidable_escaped_quote::*;
|
||||
pub(crate) use check_string_quotes::*;
|
||||
|
||||
mod from_tokens;
|
||||
mod avoidable_escaped_quote;
|
||||
mod check_string_quotes;
|
||||
|
|
|
@ -38,3 +38,21 @@ impl Default for Settings {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
#[must_use]
|
||||
pub const fn opposite(self) -> Self {
|
||||
match self {
|
||||
Self::Double => Self::Single,
|
||||
Self::Single => Self::Double,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the character used to represent this quote.
|
||||
pub const fn as_char(self) -> char {
|
||||
match self {
|
||||
Self::Double => '"',
|
||||
Self::Single => '\'',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,5 +34,184 @@ singles_escaped.py:9:5: Q003 [*] Change outer quotes to avoid escaping inner quo
|
|||
9 |- "\"string\""
|
||||
9 |+ '"string"'
|
||||
10 10 | )
|
||||
11 11 |
|
||||
12 12 | # Same as above, but with f-strings
|
||||
|
||||
singles_escaped.py:13:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
12 | # Same as above, but with f-strings
|
||||
13 | f"This is a \"string\""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
14 | f"'This' is a \"string\""
|
||||
15 | f'This is a "string"'
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
10 10 | )
|
||||
11 11 |
|
||||
12 12 | # Same as above, but with f-strings
|
||||
13 |-f"This is a \"string\""
|
||||
13 |+f'This is a "string"'
|
||||
14 14 | f"'This' is a \"string\""
|
||||
15 15 | f'This is a "string"'
|
||||
16 16 | f'\'This\' is a "string"'
|
||||
|
||||
singles_escaped.py:21:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
19 | foo = (
|
||||
20 | f"This is a"
|
||||
21 | f"\"string\""
|
||||
| ^^^^^^^^^^^^^ Q003
|
||||
22 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
18 18 | fR"This is a \"string\""
|
||||
19 19 | foo = (
|
||||
20 20 | f"This is a"
|
||||
21 |- f"\"string\""
|
||||
21 |+ f'"string"'
|
||||
22 22 | )
|
||||
23 23 |
|
||||
24 24 | # Nested f-strings (Python 3.12+)
|
||||
|
||||
singles_escaped.py:31:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
29 | #
|
||||
30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 | f"\"foo\" {"foo"}" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^ Q003
|
||||
32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
28 28 | # f'"foo" {"nested"}'
|
||||
29 29 | #
|
||||
30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 |-f"\"foo\" {"foo"}" # Q003
|
||||
31 |+f'"foo" {"foo"}' # Q003
|
||||
32 32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
34 34 |
|
||||
|
||||
singles_escaped.py:32:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 | f"\"foo\" {f"foo"}" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^ Q003
|
||||
33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
29 29 | #
|
||||
30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 |-f"\"foo\" {f"foo"}" # Q003
|
||||
32 |+f'"foo" {f"foo"}' # Q003
|
||||
33 33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
34 34 |
|
||||
35 35 | f"normal {f"nested"} normal"
|
||||
|
||||
singles_escaped.py:33:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
34 |
|
||||
35 | f"normal {f"nested"} normal"
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 |-f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
33 |+f'"foo" {f"\"foo\""} ""' # Q003
|
||||
34 34 |
|
||||
35 35 | f"normal {f"nested"} normal"
|
||||
36 36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
|
||||
singles_escaped.py:33:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
| ^^^^^^^^^^ Q003
|
||||
34 |
|
||||
35 | f"normal {f"nested"} normal"
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
30 30 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
31 31 | f"\"foo\" {"foo"}" # Q003
|
||||
32 32 | f"\"foo\" {f"foo"}" # Q003
|
||||
33 |-f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
33 |+f"\"foo\" {f'"foo"'} \"\"" # Q003
|
||||
34 34 |
|
||||
35 35 | f"normal {f"nested"} normal"
|
||||
36 36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
|
||||
singles_escaped.py:36:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
35 | f"normal {f"nested"} normal"
|
||||
36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
33 33 | f"\"foo\" {f"\"foo\""} \"\"" # Q003
|
||||
34 34 |
|
||||
35 35 | f"normal {f"nested"} normal"
|
||||
36 |-f"\"normal\" {f"nested"} normal" # Q003
|
||||
36 |+f'"normal" {f"nested"} normal' # Q003
|
||||
37 37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
39 39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
|
||||
singles_escaped.py:38:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
35 35 | f"normal {f"nested"} normal"
|
||||
36 36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
37 37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 |-f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
38 |+f"\"normal\" {f'"nested" {"other"} normal'} 'single quotes'" # Q003
|
||||
39 39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
|
||||
singles_escaped.py:39:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
36 36 | f"\"normal\" {f"nested"} normal" # Q003
|
||||
37 37 | f"\"normal\" {f"nested"} 'single quotes'"
|
||||
38 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
|
||||
39 |-f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003
|
||||
39 |+f'"normal" {f"\"nested\" {"other"} 'single quotes'"} normal' # Q003
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs
|
||||
---
|
||||
singles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
1 | this_should_raise_Q003 = "This is a \"string\""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
2 | this_is_fine = "'This' is a \"string\""
|
||||
3 | this_is_fine = 'This is a "string"'
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
1 |-this_should_raise_Q003 = "This is a \"string\""
|
||||
1 |+this_should_raise_Q003 = 'This is a "string"'
|
||||
2 2 | this_is_fine = "'This' is a \"string\""
|
||||
3 3 | this_is_fine = 'This is a "string"'
|
||||
4 4 | this_is_fine = '\'This\' is a "string"'
|
||||
|
||||
singles_escaped.py:9:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
7 | this_should_raise = (
|
||||
8 | "This is a"
|
||||
9 | "\"string\""
|
||||
| ^^^^^^^^^^^^ Q003
|
||||
10 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
6 6 | this_is_fine = R"This is a \"string\""
|
||||
7 7 | this_should_raise = (
|
||||
8 8 | "This is a"
|
||||
9 |- "\"string\""
|
||||
9 |+ '"string"'
|
||||
10 10 | )
|
||||
11 11 |
|
||||
12 12 | # Same as above, but with f-strings
|
||||
|
||||
singles_escaped.py:13:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
12 | # Same as above, but with f-strings
|
||||
13 | f"This is a \"string\""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
14 | f"'This' is a \"string\""
|
||||
15 | f'This is a "string"'
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
10 10 | )
|
||||
11 11 |
|
||||
12 12 | # Same as above, but with f-strings
|
||||
13 |-f"This is a \"string\""
|
||||
13 |+f'This is a "string"'
|
||||
14 14 | f"'This' is a \"string\""
|
||||
15 15 | f'This is a "string"'
|
||||
16 16 | f'\'This\' is a "string"'
|
||||
|
||||
singles_escaped.py:21:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
19 | foo = (
|
||||
20 | f"This is a"
|
||||
21 | f"\"string\""
|
||||
| ^^^^^^^^^^^^^ Q003
|
||||
22 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
18 18 | fR"This is a \"string\""
|
||||
19 19 | foo = (
|
||||
20 20 | f"This is a"
|
||||
21 |- f"\"string\""
|
||||
21 |+ f'"string"'
|
||||
22 22 | )
|
||||
23 23 |
|
||||
24 24 | # Nested f-strings (Python 3.12+)
|
||||
|
||||
|
|
@ -52,5 +52,205 @@ doubles_escaped.py:10:5: Q003 [*] Change outer quotes to avoid escaping inner qu
|
|||
10 |- '\'string\''
|
||||
10 |+ "'string'"
|
||||
11 11 | )
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
|
||||
doubles_escaped.py:14:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
13 | # Same as above, but with f-strings
|
||||
14 | f'This is a \'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
15 | f'This is \\ a \\\'string\'' # Q003
|
||||
16 | f'"This" is a \'string\''
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
11 11 | )
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
14 |-f'This is a \'string\'' # Q003
|
||||
14 |+f"This is a 'string'" # Q003
|
||||
15 15 | f'This is \\ a \\\'string\'' # Q003
|
||||
16 16 | f'"This" is a \'string\''
|
||||
17 17 | f"This is a 'string'"
|
||||
|
||||
doubles_escaped.py:15:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
13 | # Same as above, but with f-strings
|
||||
14 | f'This is a \'string\'' # Q003
|
||||
15 | f'This is \\ a \\\'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
16 | f'"This" is a \'string\''
|
||||
17 | f"This is a 'string'"
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
14 14 | f'This is a \'string\'' # Q003
|
||||
15 |-f'This is \\ a \\\'string\'' # Q003
|
||||
15 |+f"This is \\ a \\'string'" # Q003
|
||||
16 16 | f'"This" is a \'string\''
|
||||
17 17 | f"This is a 'string'"
|
||||
18 18 | f"\"This\" is a 'string'"
|
||||
|
||||
doubles_escaped.py:23:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
21 | foo = (
|
||||
22 | f'This is a'
|
||||
23 | f'\'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^ Q003
|
||||
24 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
20 20 | fR'This is a \'string\''
|
||||
21 21 | foo = (
|
||||
22 22 | f'This is a'
|
||||
23 |- f'\'string\'' # Q003
|
||||
23 |+ f"'string'" # Q003
|
||||
24 24 | )
|
||||
25 25 |
|
||||
26 26 | # Nested f-strings (Python 3.12+)
|
||||
|
||||
doubles_escaped.py:33:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
31 | #
|
||||
32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 | f'\'foo\' {'nested'}' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
30 30 | # f"'foo' {'nested'}"
|
||||
31 31 | #
|
||||
32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 |-f'\'foo\' {'nested'}' # Q003
|
||||
33 |+f"'foo' {'nested'}" # Q003
|
||||
34 34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
36 36 |
|
||||
|
||||
doubles_escaped.py:34:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 | f'\'foo\' {f'nested'}' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
31 31 | #
|
||||
32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 |-f'\'foo\' {f'nested'}' # Q003
|
||||
34 |+f"'foo' {f'nested'}" # Q003
|
||||
35 35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
36 36 |
|
||||
37 37 | f'normal {f'nested'} normal'
|
||||
|
||||
doubles_escaped.py:35:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
36 |
|
||||
37 | f'normal {f'nested'} normal'
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 |-f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
35 |+f"'foo' {f'\'nested\''} ''" # Q003
|
||||
36 36 |
|
||||
37 37 | f'normal {f'nested'} normal'
|
||||
38 38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
|
||||
doubles_escaped.py:35:12: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
| ^^^^^^^^^^^^^ Q003
|
||||
36 |
|
||||
37 | f'normal {f'nested'} normal'
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
32 32 | # but as the actual string itself is invalid pre 3.12, we don't catch it.
|
||||
33 33 | f'\'foo\' {'nested'}' # Q003
|
||||
34 34 | f'\'foo\' {f'nested'}' # Q003
|
||||
35 |-f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
35 |+f'\'foo\' {f"'nested'"} \'\'' # Q003
|
||||
36 36 |
|
||||
37 37 | f'normal {f'nested'} normal'
|
||||
38 38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
|
||||
doubles_escaped.py:38:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
37 | f'normal {f'nested'} normal'
|
||||
38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
35 35 | f'\'foo\' {f'\'nested\''} \'\'' # Q003
|
||||
36 36 |
|
||||
37 37 | f'normal {f'nested'} normal'
|
||||
38 |-f'\'normal\' {f'nested'} normal' # Q003
|
||||
38 |+f"'normal' {f'nested'} normal" # Q003
|
||||
39 39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
41 41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
|
||||
doubles_escaped.py:40:15: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
37 37 | f'normal {f'nested'} normal'
|
||||
38 38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
39 39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 |-f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
40 |+f'\'normal\' {f"'nested' {'other'} normal"} "double quotes"' # Q003
|
||||
41 41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
|
||||
doubles_escaped.py:41:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
38 38 | f'\'normal\' {f'nested'} normal' # Q003
|
||||
39 39 | f'\'normal\' {f'nested'} "double quotes"'
|
||||
40 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
|
||||
41 |-f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l
|
||||
41 |+f"'normal' {f'\'nested\' {'other'} "double quotes"'} normal" # Q00l
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs
|
||||
---
|
||||
doubles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
1 | this_should_raise_Q003 = 'This is a \'string\''
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
2 | this_should_raise_Q003 = 'This is \\ a \\\'string\''
|
||||
3 | this_is_fine = '"This" is a \'string\''
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
1 |-this_should_raise_Q003 = 'This is a \'string\''
|
||||
1 |+this_should_raise_Q003 = "This is a 'string'"
|
||||
2 2 | this_should_raise_Q003 = 'This is \\ a \\\'string\''
|
||||
3 3 | this_is_fine = '"This" is a \'string\''
|
||||
4 4 | this_is_fine = "This is a 'string'"
|
||||
|
||||
doubles_escaped.py:2:26: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
1 | this_should_raise_Q003 = 'This is a \'string\''
|
||||
2 | this_should_raise_Q003 = 'This is \\ a \\\'string\''
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
3 | this_is_fine = '"This" is a \'string\''
|
||||
4 | this_is_fine = "This is a 'string'"
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
1 1 | this_should_raise_Q003 = 'This is a \'string\''
|
||||
2 |-this_should_raise_Q003 = 'This is \\ a \\\'string\''
|
||||
2 |+this_should_raise_Q003 = "This is \\ a \\'string'"
|
||||
3 3 | this_is_fine = '"This" is a \'string\''
|
||||
4 4 | this_is_fine = "This is a 'string'"
|
||||
5 5 | this_is_fine = "\"This\" is a 'string'"
|
||||
|
||||
doubles_escaped.py:10:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
8 | this_should_raise = (
|
||||
9 | 'This is a'
|
||||
10 | '\'string\''
|
||||
| ^^^^^^^^^^^^ Q003
|
||||
11 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
7 7 | this_is_fine = R'This is a \'string\''
|
||||
8 8 | this_should_raise = (
|
||||
9 9 | 'This is a'
|
||||
10 |- '\'string\''
|
||||
10 |+ "'string'"
|
||||
11 11 | )
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
|
||||
doubles_escaped.py:14:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
13 | # Same as above, but with f-strings
|
||||
14 | f'This is a \'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
15 | f'This is \\ a \\\'string\'' # Q003
|
||||
16 | f'"This" is a \'string\''
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
11 11 | )
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
14 |-f'This is a \'string\'' # Q003
|
||||
14 |+f"This is a 'string'" # Q003
|
||||
15 15 | f'This is \\ a \\\'string\'' # Q003
|
||||
16 16 | f'"This" is a \'string\''
|
||||
17 17 | f"This is a 'string'"
|
||||
|
||||
doubles_escaped.py:15:1: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
13 | # Same as above, but with f-strings
|
||||
14 | f'This is a \'string\'' # Q003
|
||||
15 | f'This is \\ a \\\'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003
|
||||
16 | f'"This" is a \'string\''
|
||||
17 | f"This is a 'string'"
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
12 12 |
|
||||
13 13 | # Same as above, but with f-strings
|
||||
14 14 | f'This is a \'string\'' # Q003
|
||||
15 |-f'This is \\ a \\\'string\'' # Q003
|
||||
15 |+f"This is \\ a \\'string'" # Q003
|
||||
16 16 | f'"This" is a \'string\''
|
||||
17 17 | f"This is a 'string'"
|
||||
18 18 | f"\"This\" is a 'string'"
|
||||
|
||||
doubles_escaped.py:23:5: Q003 [*] Change outer quotes to avoid escaping inner quotes
|
||||
|
|
||||
21 | foo = (
|
||||
22 | f'This is a'
|
||||
23 | f'\'string\'' # Q003
|
||||
| ^^^^^^^^^^^^^ Q003
|
||||
24 | )
|
||||
|
|
||||
= help: Change outer quotes to avoid escaping inner quotes
|
||||
|
||||
ℹ Fix
|
||||
20 20 | fR'This is a \'string\''
|
||||
21 21 | foo = (
|
||||
22 22 | f'This is a'
|
||||
23 |- f'\'string\'' # Q003
|
||||
23 |+ f"'string'" # Q003
|
||||
24 24 | )
|
||||
25 25 |
|
||||
26 26 | # Nested f-strings (Python 3.12+)
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ mod tests {
|
|||
#[test_case(Rule::BlankLineWithWhitespace, Path::new("W29.py"))]
|
||||
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.py"))]
|
||||
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_1.py"))]
|
||||
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_2.py"))]
|
||||
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
|
||||
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
|
||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
|
||||
|
|
|
@ -2,7 +2,8 @@ use memchr::memchr_iter;
|
|||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
|
@ -45,29 +46,32 @@ impl AlwaysFixableViolation for InvalidEscapeSequence {
|
|||
pub(crate) fn invalid_escape_sequence(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
locator: &Locator,
|
||||
range: TextRange,
|
||||
indexer: &Indexer,
|
||||
token: &Tok,
|
||||
token_range: TextRange,
|
||||
fix: bool,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
|
||||
// Determine whether the string is single- or triple-quoted.
|
||||
let Some(leading_quote) = leading_quote(text) else {
|
||||
return;
|
||||
let token_source_code = match token {
|
||||
Tok::FStringMiddle { value, is_raw } => {
|
||||
if *is_raw {
|
||||
return;
|
||||
}
|
||||
value.as_str()
|
||||
}
|
||||
Tok::String { kind, .. } => {
|
||||
if kind.is_raw() {
|
||||
return;
|
||||
}
|
||||
locator.slice(token_range)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let Some(trailing_quote) = trailing_quote(text) else {
|
||||
return;
|
||||
};
|
||||
let body = &text[leading_quote.len()..text.len() - trailing_quote.len()];
|
||||
|
||||
if leading_quote.contains(['r', 'R']) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut contains_valid_escape_sequence = false;
|
||||
let mut invalid_escape_sequence = Vec::new();
|
||||
|
||||
let mut prev = None;
|
||||
let bytes = body.as_bytes();
|
||||
let bytes = token_source_code.as_bytes();
|
||||
for i in memchr_iter(b'\\', bytes) {
|
||||
// If the previous character was also a backslash, skip.
|
||||
if prev.is_some_and(|prev| prev == i - 1) {
|
||||
|
@ -77,9 +81,38 @@ pub(crate) fn invalid_escape_sequence(
|
|||
|
||||
prev = Some(i);
|
||||
|
||||
let Some(next_char) = body[i + 1..].chars().next() else {
|
||||
let next_char = match token_source_code[i + 1..].chars().next() {
|
||||
Some(next_char) => next_char,
|
||||
None if token.is_f_string_middle() => {
|
||||
// If we're at the end of a f-string middle token, the next character
|
||||
// is actually emitted as a different token. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"\{1}"
|
||||
// ```
|
||||
//
|
||||
// is lexed as `FStringMiddle('\\')` and `LBrace` (ignoring irrelevant
|
||||
// tokens), so we need to check the next character in the source code.
|
||||
//
|
||||
// Now, if we're at the end of the f-string itself, the lexer wouldn't
|
||||
// have emitted the `FStringMiddle` token in the first place. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"foo\"
|
||||
// ```
|
||||
//
|
||||
// Here, there won't be any `FStringMiddle` because it's an unterminated
|
||||
// f-string. This means that if there's a `FStringMiddle` token and we
|
||||
// encounter a `\` character, then the next character is always going to
|
||||
// be part of the f-string.
|
||||
if let Some(next_char) = locator.after(token_range.end()).chars().next() {
|
||||
next_char
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If we're at the end of the file, skip.
|
||||
continue;
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// If we're at the end of line, skip.
|
||||
|
@ -120,7 +153,7 @@ pub(crate) fn invalid_escape_sequence(
|
|||
continue;
|
||||
}
|
||||
|
||||
let location = range.start() + leading_quote.text_len() + TextSize::try_from(i).unwrap();
|
||||
let location = token_range.start() + TextSize::try_from(i).unwrap();
|
||||
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
|
||||
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
|
||||
}
|
||||
|
@ -135,14 +168,25 @@ pub(crate) fn invalid_escape_sequence(
|
|||
)));
|
||||
}
|
||||
} else {
|
||||
let tok_start = if token.is_f_string_middle() {
|
||||
// SAFETY: If this is a `FStringMiddle` token, then the indexer
|
||||
// must have the f-string range.
|
||||
indexer
|
||||
.fstring_ranges()
|
||||
.innermost(token_range.start())
|
||||
.unwrap()
|
||||
.start()
|
||||
} else {
|
||||
token_range.start()
|
||||
};
|
||||
// Turn into raw string.
|
||||
for diagnostic in &mut invalid_escape_sequence {
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||
// `returnr"foo"` is not.
|
||||
diagnostic.set_fix(Fix::automatic(Edit::insertion(
|
||||
pad_start("r".to_string(), range.start(), locator),
|
||||
range.start(),
|
||||
pad_start("r".to_string(), tok_start, locator),
|
||||
tok_start,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,12 +134,27 @@ pub(crate) fn extraneous_whitespace(
|
|||
fix_before_punctuation: bool,
|
||||
) {
|
||||
let mut prev_token = None;
|
||||
let mut fstrings = 0u32;
|
||||
|
||||
for token in line.tokens() {
|
||||
let kind = token.kind();
|
||||
match kind {
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(symbol) = BracketOrPunctuation::from_kind(kind) {
|
||||
// Whitespace before "{" or after "}" might be required in f-strings.
|
||||
// For example,
|
||||
//
|
||||
// ```python
|
||||
// f"{ {'a': 1} }"
|
||||
// ```
|
||||
//
|
||||
// Here, `{{` / `}} would be interpreted as a single raw `{` / `}`
|
||||
// character.
|
||||
match symbol {
|
||||
BracketOrPunctuation::OpenBracket(symbol) => {
|
||||
BracketOrPunctuation::OpenBracket(symbol) if symbol != '{' || fstrings == 0 => {
|
||||
let (trailing, trailing_len) = line.trailing_whitespace(token);
|
||||
if !matches!(trailing, Whitespace::None) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
|
@ -153,7 +168,7 @@ pub(crate) fn extraneous_whitespace(
|
|||
context.push_diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
BracketOrPunctuation::CloseBracket(symbol) => {
|
||||
BracketOrPunctuation::CloseBracket(symbol) if symbol != '}' || fstrings == 0 => {
|
||||
if !matches!(prev_token, Some(TokenKind::Comma)) {
|
||||
if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) =
|
||||
line.leading_whitespace(token)
|
||||
|
@ -189,6 +204,7 @@ pub(crate) fn extraneous_whitespace(
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ impl AlwaysFixableViolation for MissingWhitespace {
|
|||
/// E231
|
||||
pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut LogicalLinesContext) {
|
||||
let mut open_parentheses = 0u32;
|
||||
let mut fstrings = 0u32;
|
||||
let mut prev_lsqb = TextSize::default();
|
||||
let mut prev_lbrace = TextSize::default();
|
||||
let mut iter = line.tokens().iter().peekable();
|
||||
|
@ -62,6 +63,8 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut Lo
|
|||
while let Some(token) = iter.next() {
|
||||
let kind = token.kind();
|
||||
match kind {
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
TokenKind::Lsqb => {
|
||||
open_parentheses = open_parentheses.saturating_add(1);
|
||||
prev_lsqb = token.start();
|
||||
|
@ -72,6 +75,17 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut Lo
|
|||
TokenKind::Lbrace => {
|
||||
prev_lbrace = token.start();
|
||||
}
|
||||
TokenKind::Colon if fstrings > 0 => {
|
||||
// Colon in f-string, no space required. This will yield false
|
||||
// negatives for cases like the following as it's hard to
|
||||
// differentiate between the usage of a colon in a f-string.
|
||||
//
|
||||
// ```python
|
||||
// f'{ {'x':1} }'
|
||||
// f'{(lambda x:x)}'
|
||||
// ```
|
||||
continue;
|
||||
}
|
||||
|
||||
TokenKind::Comma | TokenKind::Semi | TokenKind::Colon => {
|
||||
let after = line.text_after(token);
|
||||
|
|
|
@ -141,6 +141,7 @@ pub(crate) fn missing_whitespace_around_operator(
|
|||
prev_token.kind(),
|
||||
TokenKind::Lpar | TokenKind::Lambda
|
||||
));
|
||||
let mut fstrings = u32::from(matches!(prev_token.kind(), TokenKind::FStringStart));
|
||||
|
||||
while let Some(token) = tokens.next() {
|
||||
let kind = token.kind();
|
||||
|
@ -150,13 +151,15 @@ pub(crate) fn missing_whitespace_around_operator(
|
|||
}
|
||||
|
||||
match kind {
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
TokenKind::Lpar | TokenKind::Lambda => parens += 1,
|
||||
TokenKind::Rpar => parens = parens.saturating_sub(1),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let needs_space = if kind == TokenKind::Equal && parens > 0 {
|
||||
// Allow keyword args or defaults: foo(bar=None).
|
||||
let needs_space = if kind == TokenKind::Equal && (parens > 0 || fstrings > 0) {
|
||||
// Allow keyword args, defaults: foo(bar=None) and f-strings: f'{foo=}'
|
||||
NeedsSpace::No
|
||||
} else if kind == TokenKind::Slash {
|
||||
// Tolerate the "/" operator in function definition
|
||||
|
|
|
@ -26,7 +26,7 @@ use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineTo
|
|||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def add(a = 0) -> int:
|
||||
/// def add(a=0) -> int:
|
||||
/// return a + 1
|
||||
/// ```
|
||||
///
|
||||
|
|
|
@ -144,4 +144,22 @@ E20.py:81:6: E201 [*] Whitespace after '['
|
|||
83 83 | #: Okay
|
||||
84 84 | x = [ #
|
||||
|
||||
E20.py:90:5: E201 [*] Whitespace after '['
|
||||
|
|
||||
88 | # F-strings
|
||||
89 | f"{ {'a': 1} }"
|
||||
90 | f"{[ { {'a': 1} } ]}"
|
||||
| ^ E201
|
||||
91 | f"normal { {f"{ { [1, 2] } }" } } normal"
|
||||
|
|
||||
= help: Remove whitespace before '['
|
||||
|
||||
ℹ Fix
|
||||
87 87 |
|
||||
88 88 | # F-strings
|
||||
89 89 | f"{ {'a': 1} }"
|
||||
90 |-f"{[ { {'a': 1} } ]}"
|
||||
90 |+f"{[{ {'a': 1} } ]}"
|
||||
91 91 | f"normal { {f"{ { [1, 2] } }" } } normal"
|
||||
|
||||
|
||||
|
|
|
@ -126,4 +126,22 @@ E20.py:29:11: E202 [*] Whitespace before ']'
|
|||
31 31 | spam(ham[1], {eggs: 2})
|
||||
32 32 |
|
||||
|
||||
E20.py:90:18: E202 [*] Whitespace before ']'
|
||||
|
|
||||
88 | # F-strings
|
||||
89 | f"{ {'a': 1} }"
|
||||
90 | f"{[ { {'a': 1} } ]}"
|
||||
| ^ E202
|
||||
91 | f"normal { {f"{ { [1, 2] } }" } } normal"
|
||||
|
|
||||
= help: Remove whitespace before ']'
|
||||
|
||||
ℹ Fix
|
||||
87 87 |
|
||||
88 88 | # F-strings
|
||||
89 89 | f"{ {'a': 1} }"
|
||||
90 |-f"{[ { {'a': 1} } ]}"
|
||||
90 |+f"{[ { {'a': 1} }]}"
|
||||
91 91 | f"normal { {f"{ { [1, 2] } }" } } normal"
|
||||
|
||||
|
||||
|
|
|
@ -99,6 +99,26 @@ E23.py:29:20: E231 [*] Missing whitespace after ':'
|
|||
29 |+ 'tag_smalldata': [('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||
30 30 | }
|
||||
31 31 |
|
||||
32 32 | #: Okay
|
||||
32 32 | # E231
|
||||
|
||||
E23.py:33:6: E231 [*] Missing whitespace after ','
|
||||
|
|
||||
32 | # E231
|
||||
33 | f"{(a,b)}"
|
||||
| ^ E231
|
||||
34 |
|
||||
35 | # Okay because it's hard to differentiate between the usages of a colon in a f-string
|
||||
|
|
||||
= help: Added missing whitespace after ','
|
||||
|
||||
ℹ Fix
|
||||
30 30 | }
|
||||
31 31 |
|
||||
32 32 | # E231
|
||||
33 |-f"{(a,b)}"
|
||||
33 |+f"{(a, b)}"
|
||||
34 34 |
|
||||
35 35 | # Okay because it's hard to differentiate between the usages of a colon in a f-string
|
||||
36 36 | f"{a:=1}"
|
||||
|
||||
|
||||
|
|
|
@ -349,4 +349,20 @@ W19.py:146:1: W191 Indentation contains tabs
|
|||
148 | #: W191 - okay
|
||||
|
|
||||
|
||||
W19.py:157:1: W191 Indentation contains tabs
|
||||
|
|
||||
156 | f"test{
|
||||
157 | tab_indented_should_be_flagged
|
||||
| ^^^^ W191
|
||||
158 | } <- this tab is fine"
|
||||
|
|
||||
|
||||
W19.py:161:1: W191 Indentation contains tabs
|
||||
|
|
||||
160 | f"""test{
|
||||
161 | tab_indented_should_be_flagged
|
||||
| ^^^^ W191
|
||||
162 | } <- this tab is fine"""
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
3 | #: W605:1:10
|
||||
4 | regex = f'\.png$'
|
||||
| ^^ W605
|
||||
5 |
|
||||
6 | #: W605:2:1
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
1 1 | # Same as `W605_0.py` but using f-strings instead.
|
||||
2 2 |
|
||||
3 3 | #: W605:1:10
|
||||
4 |-regex = f'\.png$'
|
||||
4 |+regex = rf'\.png$'
|
||||
5 5 |
|
||||
6 6 | #: W605:2:1
|
||||
7 7 | regex = f'''
|
||||
|
||||
W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
6 | #: W605:2:1
|
||||
7 | regex = f'''
|
||||
8 | \.png$
|
||||
| ^^ W605
|
||||
9 | '''
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
4 4 | regex = f'\.png$'
|
||||
5 5 |
|
||||
6 6 | #: W605:2:1
|
||||
7 |-regex = f'''
|
||||
7 |+regex = rf'''
|
||||
8 8 | \.png$
|
||||
9 9 | '''
|
||||
10 10 |
|
||||
|
||||
W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_`
|
||||
|
|
||||
11 | #: W605:2:6
|
||||
12 | f(
|
||||
13 | f'\_'
|
||||
| ^^ W605
|
||||
14 | )
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
10 10 |
|
||||
11 11 | #: W605:2:6
|
||||
12 12 | f(
|
||||
13 |- f'\_'
|
||||
13 |+ rf'\_'
|
||||
14 14 | )
|
||||
15 15 |
|
||||
16 16 | #: W605:4:6
|
||||
|
||||
W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
||||
|
|
||||
18 | multi-line
|
||||
19 | literal
|
||||
20 | with \_ somewhere
|
||||
| ^^ W605
|
||||
21 | in the middle
|
||||
22 | """
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
14 14 | )
|
||||
15 15 |
|
||||
16 16 | #: W605:4:6
|
||||
17 |-f"""
|
||||
17 |+rf"""
|
||||
18 18 | multi-line
|
||||
19 19 | literal
|
||||
20 20 | with \_ somewhere
|
||||
|
||||
W605_2.py:25:40: W605 [*] Invalid escape sequence: `\_`
|
||||
|
|
||||
24 | #: W605:1:38
|
||||
25 | value = f'new line\nand invalid escape \_ here'
|
||||
| ^^ W605
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
22 22 | """
|
||||
23 23 |
|
||||
24 24 | #: W605:1:38
|
||||
25 |-value = f'new line\nand invalid escape \_ here'
|
||||
25 |+value = f'new line\nand invalid escape \\_ here'
|
||||
26 26 |
|
||||
27 27 |
|
||||
28 28 | #: Okay
|
||||
|
||||
W605_2.py:43:13: W605 [*] Invalid escape sequence: `\_`
|
||||
|
|
||||
41 | ''' # noqa
|
||||
42 |
|
||||
43 | regex = f'\\\_'
|
||||
| ^^ W605
|
||||
44 | value = f'\{{1}}'
|
||||
45 | value = f'\{1}'
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
40 40 | \w
|
||||
41 41 | ''' # noqa
|
||||
42 42 |
|
||||
43 |-regex = f'\\\_'
|
||||
43 |+regex = f'\\\\_'
|
||||
44 44 | value = f'\{{1}}'
|
||||
45 45 | value = f'\{1}'
|
||||
46 46 | value = f'{1:\}'
|
||||
|
||||
W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
||||
|
|
||||
43 | regex = f'\\\_'
|
||||
44 | value = f'\{{1}}'
|
||||
| ^^ W605
|
||||
45 | value = f'\{1}'
|
||||
46 | value = f'{1:\}'
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
41 41 | ''' # noqa
|
||||
42 42 |
|
||||
43 43 | regex = f'\\\_'
|
||||
44 |-value = f'\{{1}}'
|
||||
44 |+value = rf'\{{1}}'
|
||||
45 45 | value = f'\{1}'
|
||||
46 46 | value = f'{1:\}'
|
||||
47 47 | value = f"{f"\{1}"}"
|
||||
|
||||
W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
||||
|
|
||||
43 | regex = f'\\\_'
|
||||
44 | value = f'\{{1}}'
|
||||
45 | value = f'\{1}'
|
||||
| ^^ W605
|
||||
46 | value = f'{1:\}'
|
||||
47 | value = f"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
42 42 |
|
||||
43 43 | regex = f'\\\_'
|
||||
44 44 | value = f'\{{1}}'
|
||||
45 |-value = f'\{1}'
|
||||
45 |+value = rf'\{1}'
|
||||
46 46 | value = f'{1:\}'
|
||||
47 47 | value = f"{f"\{1}"}"
|
||||
48 48 | value = rf"{f"\{1}"}"
|
||||
|
||||
W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
||||
|
|
||||
44 | value = f'\{{1}}'
|
||||
45 | value = f'\{1}'
|
||||
46 | value = f'{1:\}'
|
||||
| ^^ W605
|
||||
47 | value = f"{f"\{1}"}"
|
||||
48 | value = rf"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
43 43 | regex = f'\\\_'
|
||||
44 44 | value = f'\{{1}}'
|
||||
45 45 | value = f'\{1}'
|
||||
46 |-value = f'{1:\}'
|
||||
46 |+value = rf'{1:\}'
|
||||
47 47 | value = f"{f"\{1}"}"
|
||||
48 48 | value = rf"{f"\{1}"}"
|
||||
49 49 |
|
||||
|
||||
W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
||||
|
|
||||
45 | value = f'\{1}'
|
||||
46 | value = f'{1:\}'
|
||||
47 | value = f"{f"\{1}"}"
|
||||
| ^^ W605
|
||||
48 | value = rf"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
44 44 | value = f'\{{1}}'
|
||||
45 45 | value = f'\{1}'
|
||||
46 46 | value = f'{1:\}'
|
||||
47 |-value = f"{f"\{1}"}"
|
||||
47 |+value = f"{rf"\{1}"}"
|
||||
48 48 | value = rf"{f"\{1}"}"
|
||||
49 49 |
|
||||
50 50 | # Okay
|
||||
|
||||
W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
||||
|
|
||||
46 | value = f'{1:\}'
|
||||
47 | value = f"{f"\{1}"}"
|
||||
48 | value = rf"{f"\{1}"}"
|
||||
| ^^ W605
|
||||
49 |
|
||||
50 | # Okay
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
|
||||
ℹ Fix
|
||||
45 45 | value = f'\{1}'
|
||||
46 46 | value = f'{1:\}'
|
||||
47 47 | value = f"{f"\{1}"}"
|
||||
48 |-value = rf"{f"\{1}"}"
|
||||
48 |+value = rf"{rf"\{1}"}"
|
||||
49 49 |
|
||||
50 50 | # Okay
|
||||
51 51 | value = rf'\{{1}}'
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, PySourceType};
|
||||
use ruff_python_parser::{lexer, AsMode, StringKind, Tok};
|
||||
use ruff_python_ast::{self as ast, Expr, PySourceType};
|
||||
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
|
@ -47,31 +47,54 @@ impl AlwaysFixableViolation for FStringMissingPlaceholders {
|
|||
}
|
||||
}
|
||||
|
||||
/// Find f-strings that don't contain any formatted values in an [`FString`].
|
||||
fn find_useless_f_strings<'a>(
|
||||
expr: &'a Expr,
|
||||
/// Return an iterator containing a two-element tuple for each f-string part
|
||||
/// in the given [`ExprFString`] expression.
|
||||
///
|
||||
/// The first element of the tuple is the f-string prefix range, and the second
|
||||
/// element is the entire f-string range. It returns an iterator because of the
|
||||
/// possibility of multiple f-strings implicitly concatenated together.
|
||||
///
|
||||
/// For example,
|
||||
///
|
||||
/// ```python
|
||||
/// f"first" rf"second"
|
||||
/// # ^ ^ (prefix range)
|
||||
/// # ^^^^^^^^ ^^^^^^^^^^ (token range)
|
||||
/// ```
|
||||
///
|
||||
/// would return `[(0..1, 0..8), (10..11, 9..19)]`.
|
||||
///
|
||||
/// This function assumes that the given f-string expression is without any
|
||||
/// placeholder expressions.
|
||||
///
|
||||
/// [`ExprFString`]: `ruff_python_ast::ExprFString`
|
||||
fn fstring_prefix_and_tok_range<'a>(
|
||||
fstring: &'a ast::ExprFString,
|
||||
locator: &'a Locator,
|
||||
source_type: PySourceType,
|
||||
) -> impl Iterator<Item = (TextRange, TextRange)> + 'a {
|
||||
let contents = locator.slice(expr);
|
||||
lexer::lex_starts_at(contents, source_type.as_mode(), expr.start())
|
||||
let contents = locator.slice(fstring);
|
||||
let mut current_f_string_start = fstring.start();
|
||||
lexer::lex_starts_at(contents, source_type.as_mode(), fstring.start())
|
||||
.flatten()
|
||||
.filter_map(|(tok, range)| match tok {
|
||||
Tok::String {
|
||||
kind: StringKind::FString | StringKind::RawFString,
|
||||
..
|
||||
} => {
|
||||
let first_char = locator.slice(TextRange::at(range.start(), TextSize::from(1)));
|
||||
.filter_map(move |(tok, range)| match tok {
|
||||
Tok::FStringStart => {
|
||||
current_f_string_start = range.start();
|
||||
None
|
||||
}
|
||||
Tok::FStringEnd => {
|
||||
let first_char =
|
||||
locator.slice(TextRange::at(current_f_string_start, TextSize::from(1)));
|
||||
// f"..." => f_position = 0
|
||||
// fr"..." => f_position = 0
|
||||
// rf"..." => f_position = 1
|
||||
let f_position = u32::from(!(first_char == "f" || first_char == "F"));
|
||||
Some((
|
||||
TextRange::at(
|
||||
range.start() + TextSize::from(f_position),
|
||||
current_f_string_start + TextSize::from(f_position),
|
||||
TextSize::from(1),
|
||||
),
|
||||
range,
|
||||
TextRange::new(current_f_string_start, range.end()),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
|
@ -79,13 +102,14 @@ fn find_useless_f_strings<'a>(
|
|||
}
|
||||
|
||||
/// F541
|
||||
pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) {
|
||||
if !values
|
||||
pub(crate) fn f_string_missing_placeholders(fstring: &ast::ExprFString, checker: &mut Checker) {
|
||||
if !fstring
|
||||
.values
|
||||
.iter()
|
||||
.any(|value| matches!(value, Expr::FormattedValue(_)))
|
||||
{
|
||||
for (prefix_range, tok_range) in
|
||||
find_useless_f_strings(expr, checker.locator(), checker.source_type)
|
||||
fstring_prefix_and_tok_range(fstring, checker.locator(), checker.source_type)
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
|
|
|
@ -332,7 +332,7 @@ F541.py:40:3: F541 [*] f-string without any placeholders
|
|||
40 |+"" ""
|
||||
41 41 | ''f""
|
||||
42 42 | (""f""r"")
|
||||
43 43 |
|
||||
43 43 | f"{v:{f"0.2f"}}"
|
||||
|
||||
F541.py:41:3: F541 [*] f-string without any placeholders
|
||||
|
|
||||
|
@ -341,6 +341,7 @@ F541.py:41:3: F541 [*] f-string without any placeholders
|
|||
41 | ''f""
|
||||
| ^^^ F541
|
||||
42 | (""f""r"")
|
||||
43 | f"{v:{f"0.2f"}}"
|
||||
|
|
||||
= help: Remove extraneous `f` prefix
|
||||
|
||||
|
@ -351,8 +352,8 @@ F541.py:41:3: F541 [*] f-string without any placeholders
|
|||
41 |-''f""
|
||||
41 |+''""
|
||||
42 42 | (""f""r"")
|
||||
43 43 |
|
||||
44 44 | # To be fixed
|
||||
43 43 | f"{v:{f"0.2f"}}"
|
||||
44 44 | f"\{{x}}"
|
||||
|
||||
F541.py:42:4: F541 [*] f-string without any placeholders
|
||||
|
|
||||
|
@ -360,8 +361,8 @@ F541.py:42:4: F541 [*] f-string without any placeholders
|
|||
41 | ''f""
|
||||
42 | (""f""r"")
|
||||
| ^^^ F541
|
||||
43 |
|
||||
44 | # To be fixed
|
||||
43 | f"{v:{f"0.2f"}}"
|
||||
44 | f"\{{x}}"
|
||||
|
|
||||
= help: Remove extraneous `f` prefix
|
||||
|
||||
|
@ -371,8 +372,41 @@ F541.py:42:4: F541 [*] f-string without any placeholders
|
|||
41 41 | ''f""
|
||||
42 |-(""f""r"")
|
||||
42 |+("" ""r"")
|
||||
43 43 |
|
||||
44 44 | # To be fixed
|
||||
45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8
|
||||
43 43 | f"{v:{f"0.2f"}}"
|
||||
44 44 | f"\{{x}}"
|
||||
|
||||
F541.py:43:7: F541 [*] f-string without any placeholders
|
||||
|
|
||||
41 | ''f""
|
||||
42 | (""f""r"")
|
||||
43 | f"{v:{f"0.2f"}}"
|
||||
| ^^^^^^^ F541
|
||||
44 | f"\{{x}}"
|
||||
|
|
||||
= help: Remove extraneous `f` prefix
|
||||
|
||||
ℹ Fix
|
||||
40 40 | ""f""
|
||||
41 41 | ''f""
|
||||
42 42 | (""f""r"")
|
||||
43 |-f"{v:{f"0.2f"}}"
|
||||
43 |+f"{v:{"0.2f"}}"
|
||||
44 44 | f"\{{x}}"
|
||||
|
||||
F541.py:44:1: F541 [*] f-string without any placeholders
|
||||
|
|
||||
42 | (""f""r"")
|
||||
43 | f"{v:{f"0.2f"}}"
|
||||
44 | f"\{{x}}"
|
||||
| ^^^^^^^^^ F541
|
||||
|
|
||||
= help: Remove extraneous `f` prefix
|
||||
|
||||
ℹ Fix
|
||||
41 41 | ''f""
|
||||
42 42 | (""f""r"")
|
||||
43 43 | f"{v:{f"0.2f"}}"
|
||||
44 |-f"\{{x}}"
|
||||
44 |+"\{x}"
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use ruff_diagnostics::AlwaysFixableViolation;
|
|||
use ruff_diagnostics::Edit;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
/// ## What it does
|
||||
|
@ -173,10 +174,15 @@ impl AlwaysFixableViolation for InvalidCharacterZeroWidthSpace {
|
|||
/// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515
|
||||
pub(crate) fn invalid_string_characters(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
tok: &Tok,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
let text = match tok {
|
||||
Tok::String { .. } => locator.slice(range),
|
||||
Tok::FStringMiddle { value, .. } => value.as_str(),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) {
|
||||
let c = match_.chars().next().unwrap();
|
||||
|
|
|
@ -7,8 +7,7 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u
|
|||
14 | #foo = 'hi'
|
||||
15 | b = ''
|
||||
| PLE2510
|
||||
16 |
|
||||
17 | b_ok = '\\b'
|
||||
16 | b = f''
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
|
@ -18,8 +17,45 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u
|
|||
14 14 | #foo = 'hi'
|
||||
15 |-b = ''
|
||||
15 |+b = '\b'
|
||||
16 16 |
|
||||
17 17 | b_ok = '\\b'
|
||||
18 18 |
|
||||
16 16 | b = f''
|
||||
17 17 |
|
||||
18 18 | b_ok = '\\b'
|
||||
|
||||
invalid_characters.py:16:7: PLE2510 [*] Invalid unescaped character backspace, use "\b" instead
|
||||
|
|
||||
14 | #foo = 'hi'
|
||||
15 | b = ''
|
||||
16 | b = f''
|
||||
| PLE2510
|
||||
17 |
|
||||
18 | b_ok = '\\b'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
13 13 | # (Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall,
|
||||
14 14 | #foo = 'hi'
|
||||
15 15 | b = ''
|
||||
16 |-b = f''
|
||||
16 |+b = f'\b'
|
||||
17 17 |
|
||||
18 18 | b_ok = '\\b'
|
||||
19 19 | b_ok = f'\\b'
|
||||
|
||||
invalid_characters.py:55:21: PLE2510 [*] Invalid unescaped character backspace, use "\b" instead
|
||||
|
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 |
|
||||
55 | nested_fstrings = f'{f'{f''}'}'
|
||||
| PLE2510
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
55 |-nested_fstrings = f'{f'{f''}'}'
|
||||
55 |+nested_fstrings = f'\b{f'{f''}'}'
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,60 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
invalid_characters.py:21:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
|
||||
invalid_characters.py:24:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
|
||||
|
|
||||
19 | cr_ok = '\\r'
|
||||
20 |
|
||||
21 | sub = 'sub '
|
||||
22 | cr_ok = f'\\r'
|
||||
23 |
|
||||
24 | sub = 'sub '
|
||||
| PLE2512
|
||||
22 |
|
||||
23 | sub_ok = '\x1a'
|
||||
25 | sub = f'sub '
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
18 18 |
|
||||
19 19 | cr_ok = '\\r'
|
||||
20 20 |
|
||||
21 |-sub = 'sub '
|
||||
21 |+sub = 'sub \x1A'
|
||||
22 22 |
|
||||
23 23 | sub_ok = '\x1a'
|
||||
24 24 |
|
||||
21 21 | cr_ok = '\\r'
|
||||
22 22 | cr_ok = f'\\r'
|
||||
23 23 |
|
||||
24 |-sub = 'sub '
|
||||
24 |+sub = 'sub \x1A'
|
||||
25 25 | sub = f'sub '
|
||||
26 26 |
|
||||
27 27 | sub_ok = '\x1a'
|
||||
|
||||
invalid_characters.py:25:13: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
|
||||
|
|
||||
24 | sub = 'sub '
|
||||
25 | sub = f'sub '
|
||||
| PLE2512
|
||||
26 |
|
||||
27 | sub_ok = '\x1a'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
22 22 | cr_ok = f'\\r'
|
||||
23 23 |
|
||||
24 24 | sub = 'sub '
|
||||
25 |-sub = f'sub '
|
||||
25 |+sub = f'sub \x1A'
|
||||
26 26 |
|
||||
27 27 | sub_ok = '\x1a'
|
||||
28 28 | sub_ok = f'\x1a'
|
||||
|
||||
invalid_characters.py:55:25: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
|
||||
|
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 |
|
||||
55 | nested_fstrings = f'{f'{f''}'}'
|
||||
| PLE2512
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
55 |-nested_fstrings = f'{f'{f''}'}'
|
||||
55 |+nested_fstrings = f'{f'\x1A{f''}'}'
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,60 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
invalid_characters.py:25:16: PLE2513 [*] Invalid unescaped character ESC, use "\x1B" instead
|
||||
invalid_characters.py:30:16: PLE2513 [*] Invalid unescaped character ESC, use "\x1B" instead
|
||||
|
|
||||
23 | sub_ok = '\x1a'
|
||||
24 |
|
||||
25 | esc = 'esc esc '
|
||||
28 | sub_ok = f'\x1a'
|
||||
29 |
|
||||
30 | esc = 'esc esc '
|
||||
| PLE2513
|
||||
26 |
|
||||
27 | esc_ok = '\x1b'
|
||||
31 | esc = f'esc esc '
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
22 22 |
|
||||
23 23 | sub_ok = '\x1a'
|
||||
24 24 |
|
||||
25 |-esc = 'esc esc '
|
||||
25 |+esc = 'esc esc \x1B'
|
||||
26 26 |
|
||||
27 27 | esc_ok = '\x1b'
|
||||
28 28 |
|
||||
27 27 | sub_ok = '\x1a'
|
||||
28 28 | sub_ok = f'\x1a'
|
||||
29 29 |
|
||||
30 |-esc = 'esc esc '
|
||||
30 |+esc = 'esc esc \x1B'
|
||||
31 31 | esc = f'esc esc '
|
||||
32 32 |
|
||||
33 33 | esc_ok = '\x1b'
|
||||
|
||||
invalid_characters.py:31:17: PLE2513 [*] Invalid unescaped character ESC, use "\x1B" instead
|
||||
|
|
||||
30 | esc = 'esc esc '
|
||||
31 | esc = f'esc esc '
|
||||
| PLE2513
|
||||
32 |
|
||||
33 | esc_ok = '\x1b'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
28 28 | sub_ok = f'\x1a'
|
||||
29 29 |
|
||||
30 30 | esc = 'esc esc '
|
||||
31 |-esc = f'esc esc '
|
||||
31 |+esc = f'esc esc \x1B'
|
||||
32 32 |
|
||||
33 33 | esc_ok = '\x1b'
|
||||
34 34 | esc_ok = f'\x1b'
|
||||
|
||||
invalid_characters.py:55:29: PLE2513 [*] Invalid unescaped character ESC, use "\x1B" instead
|
||||
|
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 |
|
||||
55 | nested_fstrings = f'{f'{f''}'}'
|
||||
| PLE2513
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
55 |-nested_fstrings = f'{f'{f''}'}'
|
||||
55 |+nested_fstrings = f'{f'{f'\x1B'}'}'
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,73 +1,165 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
invalid_characters.py:34:13: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
invalid_characters.py:44:13: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
32 | nul_ok = '\0'
|
||||
33 |
|
||||
34 | zwsp = 'zerowidth'
|
||||
42 | nul_ok = f'\0'
|
||||
43 |
|
||||
44 | zwsp = 'zerowidth'
|
||||
| PLE2515
|
||||
35 |
|
||||
36 | zwsp_ok = '\u200b'
|
||||
45 | zwsp = f'zerowidth'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
31 31 |
|
||||
32 32 | nul_ok = '\0'
|
||||
33 33 |
|
||||
34 |-zwsp = 'zerowidth'
|
||||
34 |+zwsp = 'zero\u200bwidth'
|
||||
35 35 |
|
||||
36 36 | zwsp_ok = '\u200b'
|
||||
37 37 |
|
||||
41 41 | nul_ok = '\0'
|
||||
42 42 | nul_ok = f'\0'
|
||||
43 43 |
|
||||
44 |-zwsp = 'zerowidth'
|
||||
44 |+zwsp = 'zero\u200bwidth'
|
||||
45 45 | zwsp = f'zerowidth'
|
||||
46 46 |
|
||||
47 47 | zwsp_ok = '\u200b'
|
||||
|
||||
invalid_characters.py:38:36: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
invalid_characters.py:45:14: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
36 | zwsp_ok = '\u200b'
|
||||
37 |
|
||||
38 | zwsp_after_multibyte_character = "ಫ"
|
||||
44 | zwsp = 'zerowidth'
|
||||
45 | zwsp = f'zerowidth'
|
||||
| PLE2515
|
||||
46 |
|
||||
47 | zwsp_ok = '\u200b'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
42 42 | nul_ok = f'\0'
|
||||
43 43 |
|
||||
44 44 | zwsp = 'zerowidth'
|
||||
45 |-zwsp = f'zerowidth'
|
||||
45 |+zwsp = f'zero\u200bwidth'
|
||||
46 46 |
|
||||
47 47 | zwsp_ok = '\u200b'
|
||||
48 48 | zwsp_ok = f'\u200b'
|
||||
|
||||
invalid_characters.py:50:36: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
48 | zwsp_ok = f'\u200b'
|
||||
49 |
|
||||
50 | zwsp_after_multibyte_character = "ಫ"
|
||||
| PLE2515
|
||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
35 35 |
|
||||
36 36 | zwsp_ok = '\u200b'
|
||||
37 37 |
|
||||
38 |-zwsp_after_multibyte_character = "ಫ"
|
||||
38 |+zwsp_after_multibyte_character = "ಫ\u200b"
|
||||
39 39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
47 47 | zwsp_ok = '\u200b'
|
||||
48 48 | zwsp_ok = f'\u200b'
|
||||
49 49 |
|
||||
50 |-zwsp_after_multibyte_character = "ಫ"
|
||||
50 |+zwsp_after_multibyte_character = "ಫ\u200b"
|
||||
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
|
||||
invalid_characters.py:39:60: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
invalid_characters.py:51:37: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
38 | zwsp_after_multibyte_character = "ಫ"
|
||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
| PLE2515
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
48 48 | zwsp_ok = f'\u200b'
|
||||
49 49 |
|
||||
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 |-zwsp_after_multibyte_character = f"ಫ"
|
||||
51 |+zwsp_after_multibyte_character = f"ಫ\u200b"
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
|
||||
invalid_characters.py:52:60: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
| PLE2515
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
36 36 | zwsp_ok = '\u200b'
|
||||
37 37 |
|
||||
38 38 | zwsp_after_multibyte_character = "ಫ"
|
||||
39 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
39 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
49 49 |
|
||||
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
52 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
55 55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
||||
invalid_characters.py:39:61: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
invalid_characters.py:52:61: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
38 | zwsp_after_multibyte_character = "ಫ"
|
||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
| PLE2515
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
36 36 | zwsp_ok = '\u200b'
|
||||
37 37 |
|
||||
38 38 | zwsp_after_multibyte_character = "ಫ"
|
||||
39 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
39 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
49 49 |
|
||||
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
52 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
54 54 |
|
||||
55 55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
||||
invalid_characters.py:53:61: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
| PLE2515
|
||||
54 |
|
||||
55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 |-zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 |+zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
54 54 |
|
||||
55 55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
||||
invalid_characters.py:53:62: PLE2515 [*] Invalid unescaped character zero-width-space, use "\u200B" instead
|
||||
|
|
||||
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
| PLE2515
|
||||
54 |
|
||||
55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
|
||||
= help: Replace with escape sequence
|
||||
|
||||
ℹ Fix
|
||||
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||
52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 |-zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||
53 |+zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
||||
54 54 |
|
||||
55 55 | nested_fstrings = f'{f'{f''}'}'
|
||||
|
||||
|
||||
|
|
|
@ -123,6 +123,7 @@ impl Violation for AmbiguousUnicodeCharacterComment {
|
|||
}
|
||||
}
|
||||
|
||||
/// RUF001, RUF002, RUF003
|
||||
pub(crate) fn ambiguous_unicode_character(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
locator: &Locator,
|
||||
|
|
|
@ -49,6 +49,8 @@ confusables.py:31:6: RUF001 String contains ambiguous `Р` (CYRILLIC CAPITAL LET
|
|||
30 | # boundary" (whitespace) that it itself ambiguous.
|
||||
31 | x = "Р усский"
|
||||
| ^ RUF001
|
||||
32 |
|
||||
33 | # Same test cases as above but using f-strings instead:
|
||||
|
|
||||
|
||||
confusables.py:31:7: RUF001 String contains ambiguous ` ` (EN QUAD). Did you mean ` ` (SPACE)?
|
||||
|
@ -57,6 +59,100 @@ confusables.py:31:7: RUF001 String contains ambiguous ` ` (EN QUAD). Did you m
|
|||
30 | # boundary" (whitespace) that it itself ambiguous.
|
||||
31 | x = "Р усский"
|
||||
| ^ RUF001
|
||||
32 |
|
||||
33 | # Same test cases as above but using f-strings instead:
|
||||
|
|
||||
|
||||
confusables.py:34:7: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
33 | # Same test cases as above but using f-strings instead:
|
||||
34 | x = f"𝐁ad string"
|
||||
| ^ RUF001
|
||||
35 | x = f"−"
|
||||
36 | x = f"Русский"
|
||||
|
|
||||
|
||||
confusables.py:37:11: RUF001 String contains ambiguous `α` (GREEK SMALL LETTER ALPHA). Did you mean `a` (LATIN SMALL LETTER A)?
|
||||
|
|
||||
35 | x = f"−"
|
||||
36 | x = f"Русский"
|
||||
37 | x = f"βα Bαd"
|
||||
| ^ RUF001
|
||||
38 | x = f"Р усский"
|
||||
|
|
||||
|
||||
confusables.py:38:7: RUF001 String contains ambiguous `Р` (CYRILLIC CAPITAL LETTER ER). Did you mean `P` (LATIN CAPITAL LETTER P)?
|
||||
|
|
||||
36 | x = f"Русский"
|
||||
37 | x = f"βα Bαd"
|
||||
38 | x = f"Р усский"
|
||||
| ^ RUF001
|
||||
39 |
|
||||
40 | # Nested f-strings
|
||||
|
|
||||
|
||||
confusables.py:38:8: RUF001 String contains ambiguous ` ` (EN QUAD). Did you mean ` ` (SPACE)?
|
||||
|
|
||||
36 | x = f"Русский"
|
||||
37 | x = f"βα Bαd"
|
||||
38 | x = f"Р усский"
|
||||
| ^ RUF001
|
||||
39 |
|
||||
40 | # Nested f-strings
|
||||
|
|
||||
|
||||
confusables.py:41:7: RUF001 String contains ambiguous `𝐁` (MATHEMATICAL BOLD CAPITAL B). Did you mean `B` (LATIN CAPITAL LETTER B)?
|
||||
|
|
||||
40 | # Nested f-strings
|
||||
41 | x = f"𝐁ad string {f" {f"Р усский"}"}"
|
||||
| ^ RUF001
|
||||
42 |
|
||||
43 | # Comments inside f-strings
|
||||
|
|
||||
|
||||
confusables.py:41:21: RUF001 String contains ambiguous ` ` (EN QUAD). Did you mean ` ` (SPACE)?
|
||||
|
|
||||
40 | # Nested f-strings
|
||||
41 | x = f"𝐁ad string {f" {f"Р усский"}"}"
|
||||
| ^ RUF001
|
||||
42 |
|
||||
43 | # Comments inside f-strings
|
||||
|
|
||||
|
||||
confusables.py:41:25: RUF001 String contains ambiguous `Р` (CYRILLIC CAPITAL LETTER ER). Did you mean `P` (LATIN CAPITAL LETTER P)?
|
||||
|
|
||||
40 | # Nested f-strings
|
||||
41 | x = f"𝐁ad string {f" {f"Р усский"}"}"
|
||||
| ^ RUF001
|
||||
42 |
|
||||
43 | # Comments inside f-strings
|
||||
|
|
||||
|
||||
confusables.py:41:26: RUF001 String contains ambiguous ` ` (EN QUAD). Did you mean ` ` (SPACE)?
|
||||
|
|
||||
40 | # Nested f-strings
|
||||
41 | x = f"𝐁ad string {f" {f"Р усский"}"}"
|
||||
| ^ RUF001
|
||||
42 |
|
||||
43 | # Comments inside f-strings
|
||||
|
|
||||
|
||||
confusables.py:44:68: RUF003 Comment contains ambiguous `)` (FULLWIDTH RIGHT PARENTHESIS). Did you mean `)` (RIGHT PARENTHESIS)?
|
||||
|
|
||||
43 | # Comments inside f-strings
|
||||
44 | x = f"string { # And here's a comment with an unusual parenthesis: )
|
||||
| ^^ RUF003
|
||||
45 | # And here's a comment with a greek alpha: ∗
|
||||
46 | foo # And here's a comment with an unusual punctuation mark: ᜵
|
||||
|
|
||||
|
||||
confusables.py:46:62: RUF003 Comment contains ambiguous `᜵` (PHILIPPINE SINGLE PUNCTUATION). Did you mean `/` (SOLIDUS)?
|
||||
|
|
||||
44 | x = f"string { # And here's a comment with an unusual parenthesis: )
|
||||
45 | # And here's a comment with a greek alpha: ∗
|
||||
46 | foo # And here's a comment with an unusual punctuation mark: ᜵
|
||||
| ^ RUF003
|
||||
47 | }"
|
||||
|
|
||||
|
||||
|
||||
|
|
|
@ -90,6 +90,13 @@ impl PythonVersion {
|
|||
}
|
||||
minimum_version
|
||||
}
|
||||
|
||||
/// Return `true` if the current version supports [PEP 701].
|
||||
///
|
||||
/// [PEP 701]: https://peps.python.org/pep-0701/
|
||||
pub fn supports_pep701(self) -> bool {
|
||||
self >= Self::Py312
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, CacheKey, is_macro::Is)]
|
||||
|
|
|
@ -2600,6 +2600,14 @@ impl Constant {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the constant is a string constant that is a unicode string (i.e., `u"..."`).
|
||||
pub fn is_unicode_string(&self) -> bool {
|
||||
match self {
|
||||
Constant::Str(value) => value.unicode,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -2620,6 +2628,16 @@ impl Deref for StringConstant {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<String> for StringConstant {
|
||||
fn from(value: String) -> StringConstant {
|
||||
Self {
|
||||
value,
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BytesConstant {
|
||||
/// The bytes value as resolved by the parser (i.e., without quotes, or escape sequences, or
|
||||
|
@ -2636,6 +2654,15 @@ impl Deref for BytesConstant {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for BytesConstant {
|
||||
fn from(value: Vec<u8>) -> BytesConstant {
|
||||
Self {
|
||||
value,
|
||||
implicit_concatenated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Constant {
|
||||
fn from(value: Vec<u8>) -> Constant {
|
||||
Self::Bytes(BytesConstant {
|
||||
|
@ -3207,6 +3234,12 @@ pub struct ParenthesizedExpr {
|
|||
/// The underlying expression.
|
||||
pub expr: Expr,
|
||||
}
|
||||
impl ParenthesizedExpr {
|
||||
/// Returns `true` if the expression is may be parenthesized.
|
||||
pub fn is_parenthesized(&self) -> bool {
|
||||
self.range != self.expr.range()
|
||||
}
|
||||
}
|
||||
impl Ranged for ParenthesizedExpr {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
|
|
|
@ -130,7 +130,7 @@ fn function_type_parameters() {
|
|||
|
||||
fn trace_preorder_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens, Mode::Module, "test.py").unwrap();
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module, "test.py").unwrap();
|
||||
|
||||
let mut visitor = RecordVisitor::default();
|
||||
visitor.visit_mod(&parsed);
|
||||
|
|
|
@ -131,7 +131,7 @@ fn function_type_parameters() {
|
|||
|
||||
fn trace_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens, Mode::Module, "test.py").unwrap();
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module, "test.py").unwrap();
|
||||
|
||||
let mut visitor = RecordVisitor::default();
|
||||
walk_module(&mut visitor, &parsed);
|
||||
|
|
|
@ -55,6 +55,9 @@ fn detect_quote(tokens: &[LexResult], locator: &Locator) -> Quote {
|
|||
triple_quoted: false,
|
||||
..
|
||||
} => Some(*range),
|
||||
// No need to check if it's triple-quoted as f-strings cannot be used
|
||||
// as docstrings.
|
||||
Tok::FStringStart => Some(*range),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
|
@ -275,6 +278,14 @@ class FormFeedIndent:
|
|||
Quote::Single
|
||||
);
|
||||
|
||||
let contents = r#"x = f'1'"#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Single
|
||||
);
|
||||
|
||||
let contents = r#"x = "1""#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
|
@ -283,6 +294,14 @@ class FormFeedIndent:
|
|||
Quote::Double
|
||||
);
|
||||
|
||||
let contents = r#"x = f"1""#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Double
|
||||
);
|
||||
|
||||
let contents = r#"s = "It's done.""#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
|
@ -328,6 +347,41 @@ a = "v"
|
|||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Double
|
||||
);
|
||||
|
||||
// Detect from f-string appearing after docstring
|
||||
let contents = r#"
|
||||
"""Module docstring."""
|
||||
|
||||
a = f'v'
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Single
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
'''Module docstring.'''
|
||||
|
||||
a = f"v"
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Double
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
f'''Module docstring.'''
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||
Quote::Single
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -43,8 +43,8 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Re
|
|||
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
||||
|
||||
// Parse the AST.
|
||||
let module =
|
||||
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")
|
||||
.context("Syntax error in input")?;
|
||||
|
||||
let options = PyFormatOptions::from_extension(source_type);
|
||||
|
||||
|
|
|
@ -567,7 +567,7 @@ mod tests {
|
|||
let source_code = SourceCode::new(source);
|
||||
let (tokens, comment_ranges) =
|
||||
tokens_and_ranges(source).expect("Expect source to be valid Python");
|
||||
let parsed = parse_ok_tokens(tokens, Mode::Module, "test.py")
|
||||
let parsed = parse_ok_tokens(tokens, source, Mode::Module, "test.py")
|
||||
.expect("Expect source to be valid Python");
|
||||
|
||||
CommentsTestCase {
|
||||
|
|
|
@ -139,7 +139,7 @@ impl<'a> FormatString<'a> {
|
|||
impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let locator = f.context().locator();
|
||||
match self.layout {
|
||||
let result = match self.layout {
|
||||
StringLayout::Default => {
|
||||
if self.string.is_implicit_concatenated() {
|
||||
in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f)
|
||||
|
@ -162,7 +162,73 @@ impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
|||
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
|
||||
FormatStringContinuation::new(self.string).fmt(f)
|
||||
}
|
||||
};
|
||||
// TODO(dhruvmanila): With PEP 701, comments can be inside f-strings.
|
||||
// This is to mark all of those comments as formatted but we need to
|
||||
// figure out how to handle them. Note that this needs to be done only
|
||||
// after the f-string is formatted, so only for all the non-formatted
|
||||
// comments.
|
||||
if let AnyString::FString(fstring) = self.string {
|
||||
let comments = f.context().comments();
|
||||
fstring.values.iter().for_each(|value| {
|
||||
comments.mark_verbatim_node_comments_formatted(value.into());
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 => {
|
||||
// We can assume that this will never overflow because we know
|
||||
// that the program once parsed to a valid AST which means that
|
||||
// the start and end tokens for f-strings are balanced.
|
||||
self.nesting -= 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,6 +261,10 @@ impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
|
|||
// because this is a black preview style.
|
||||
let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start());
|
||||
|
||||
// The lexer emits multiple tokens for a single f-string literal. Each token
|
||||
// will have it's own range but we require the complete range of the f-string.
|
||||
let mut fstring_range_builder = FStringRangeBuilder::default();
|
||||
|
||||
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
|
||||
|
||||
for token in lexer {
|
||||
|
@ -226,8 +296,31 @@ impl Format<PyFormatContext<'_>> for FormatStringContinuation<'_> {
|
|||
}
|
||||
};
|
||||
|
||||
fstring_range_builder.visit_token(&token, token_range);
|
||||
|
||||
// We need to ignore all the tokens within the f-string as there can
|
||||
// be `String` tokens inside it as well. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"foo {'bar'} foo"
|
||||
// # ^^^^^
|
||||
// # Ignore any logic for this `String` token
|
||||
// ```
|
||||
//
|
||||
// Here, we're interested in the complete f-string, not the individual
|
||||
// tokens inside it.
|
||||
if fstring_range_builder.in_fstring() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match token {
|
||||
Tok::String { .. } => {
|
||||
Tok::String { .. } | Tok::FStringEnd => {
|
||||
let token_range = if token.is_f_string_end() {
|
||||
fstring_range_builder.finish()
|
||||
} else {
|
||||
token_range
|
||||
};
|
||||
|
||||
// ```python
|
||||
// (
|
||||
// "a"
|
||||
|
@ -346,11 +439,7 @@ impl StringPart {
|
|||
}
|
||||
};
|
||||
|
||||
let normalized = normalize_string(
|
||||
locator.slice(self.content_range),
|
||||
quotes,
|
||||
self.prefix.is_raw_string(),
|
||||
);
|
||||
let normalized = normalize_string(locator.slice(self.content_range), quotes, self.prefix);
|
||||
|
||||
NormalizedString {
|
||||
prefix: self.prefix,
|
||||
|
@ -442,6 +531,10 @@ impl StringPrefix {
|
|||
pub(super) const fn is_raw_string(self) -> bool {
|
||||
self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER)
|
||||
}
|
||||
|
||||
pub(super) const fn is_fstring(self) -> bool {
|
||||
self.contains(StringPrefix::F_STRING)
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for StringPrefix {
|
||||
|
@ -681,7 +774,7 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
|
|||
/// with the provided [`StringQuotes`] style.
|
||||
///
|
||||
/// Returns the normalized string and whether it contains new lines.
|
||||
fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str> {
|
||||
fn normalize_string(input: &str, quotes: StringQuotes, prefix: StringPrefix) -> Cow<str> {
|
||||
// The normalized string if `input` is not yet normalized.
|
||||
// `output` must remain empty if `input` is already normalized.
|
||||
let mut output = String::new();
|
||||
|
@ -693,14 +786,30 @@ fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str>
|
|||
let preferred_quote = style.as_char();
|
||||
let opposite_quote = style.invert().as_char();
|
||||
|
||||
let mut chars = input.char_indices();
|
||||
let mut chars = input.char_indices().peekable();
|
||||
|
||||
let is_raw = prefix.is_raw_string();
|
||||
let is_fstring = prefix.is_fstring();
|
||||
let mut formatted_value_nesting = 0u32;
|
||||
|
||||
while let Some((index, c)) = chars.next() {
|
||||
if is_fstring && matches!(c, '{' | '}') {
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
||||
// Skip over the second character of the double braces
|
||||
chars.next();
|
||||
} else if c == '{' {
|
||||
formatted_value_nesting += 1;
|
||||
} else {
|
||||
// Safe to assume that `c == '}'` here because of the matched pattern above
|
||||
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '\r' {
|
||||
output.push_str(&input[last_index..index]);
|
||||
|
||||
// Skip over the '\r' character, keep the `\n`
|
||||
if input.as_bytes().get(index + 1).copied() == Some(b'\n') {
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == '\n') {
|
||||
chars.next();
|
||||
}
|
||||
// Replace the `\r` with a `\n`
|
||||
|
@ -711,9 +820,9 @@ fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str>
|
|||
last_index = index + '\r'.len_utf8();
|
||||
} else if !quotes.triple && !is_raw {
|
||||
if c == '\\' {
|
||||
if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) {
|
||||
if let Some((_, next)) = chars.peek().copied() {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if next == opposite_quote {
|
||||
if next == opposite_quote && formatted_value_nesting == 0 {
|
||||
// Remove the escape by ending before the backslash and starting again with the quote
|
||||
chars.next();
|
||||
output.push_str(&input[last_index..index]);
|
||||
|
@ -726,7 +835,7 @@ fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str>
|
|||
chars.next();
|
||||
}
|
||||
}
|
||||
} else if c == preferred_quote {
|
||||
} else if c == preferred_quote && formatted_value_nesting == 0 {
|
||||
// Escape the quote
|
||||
output.push_str(&input[last_index..index]);
|
||||
output.push('\\');
|
||||
|
|
|
@ -127,7 +127,7 @@ pub fn format_module_source(
|
|||
options: PyFormatOptions,
|
||||
) -> Result<Printed, FormatModuleError> {
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
|
||||
let module = parse_ok_tokens(tokens, Mode::Module, "<filename>")?;
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")?;
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
|
||||
Ok(formatted.print()?)
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ def main() -> None:
|
|||
|
||||
// Parse the AST.
|
||||
let source_path = "code_inline.py";
|
||||
let module = parse_ok_tokens(tokens, Mode::Module, source_path).unwrap();
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, source_path).unwrap();
|
||||
let options = PyFormatOptions::from_extension(Path::new(source_path));
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();
|
||||
|
||||
|
|
95
crates/ruff_python_index/src/fstring_ranges.rs
Normal file
95
crates/ruff_python_index/src/fstring_ranges.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
/// Stores the ranges of all f-strings in a file sorted by [`TextRange::start`].
|
||||
/// There can be multiple overlapping ranges for nested f-strings.
|
||||
#[derive(Debug)]
|
||||
pub struct FStringRanges {
|
||||
raw: BTreeMap<TextSize, TextRange>,
|
||||
}
|
||||
|
||||
impl FStringRanges {
|
||||
/// Return the [`TextRange`] of the innermost f-string at the given offset.
|
||||
pub fn innermost(&self, offset: TextSize) -> Option<TextRange> {
|
||||
self.raw
|
||||
.range(..=offset)
|
||||
.rev()
|
||||
.find(|(_, range)| range.contains(offset))
|
||||
.map(|(_, range)| *range)
|
||||
}
|
||||
|
||||
/// Return the [`TextRange`] of the outermost f-string at the given offset.
|
||||
pub fn outermost(&self, offset: TextSize) -> Option<TextRange> {
|
||||
// Explanation of the algorithm:
|
||||
//
|
||||
// ```python
|
||||
// # v
|
||||
// f"normal" f"another" f"first {f"second {f"third"} second"} first"
|
||||
// # ^^(1)^^^
|
||||
// # ^^^^^^^^^^^^(2)^^^^^^^^^^^^
|
||||
// # ^^^^^^^^^^^^^^^^^^^^^(3)^^^^^^^^^^^^^^^^^^^^
|
||||
// # ^^^(4)^^^^
|
||||
// # ^^^(5)^^^
|
||||
// ```
|
||||
//
|
||||
// The offset is marked with a `v` and the ranges are numbered in the order
|
||||
// they are yielded by the iterator in the reverse order. The algorithm
|
||||
// works as follows:
|
||||
// 1. Skip all ranges that don't contain the offset (1).
|
||||
// 2. Take all ranges that contain the offset (2, 3).
|
||||
// 3. Stop taking ranges when the offset is no longer contained.
|
||||
// 4. Take the last range that contained the offset (3, the outermost).
|
||||
self.raw
|
||||
.range(..=offset)
|
||||
.rev()
|
||||
.skip_while(|(_, range)| !range.contains(offset))
|
||||
.take_while(|(_, range)| range.contains(offset))
|
||||
.last()
|
||||
.map(|(_, range)| *range)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all f-string [`TextRange`] sorted by their
|
||||
/// start location.
|
||||
///
|
||||
/// For nested f-strings, the outermost f-string is yielded first, moving
|
||||
/// inwards with each iteration.
|
||||
#[inline]
|
||||
pub fn values(&self) -> impl Iterator<Item = &TextRange> + '_ {
|
||||
self.raw.values()
|
||||
}
|
||||
|
||||
/// Returns the number of f-string ranges stored.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.raw.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct FStringRangesBuilder {
|
||||
start_locations: Vec<TextSize>,
|
||||
raw: BTreeMap<TextSize, TextRange>,
|
||||
}
|
||||
|
||||
impl FStringRangesBuilder {
|
||||
pub(crate) fn visit_token(&mut self, token: &Tok, range: TextRange) {
|
||||
match token {
|
||||
Tok::FStringStart => {
|
||||
self.start_locations.push(range.start());
|
||||
}
|
||||
Tok::FStringEnd => {
|
||||
if let Some(start) = self.start_locations.pop() {
|
||||
self.raw.insert(start, TextRange::new(start, range.end()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(self) -> FStringRanges {
|
||||
debug_assert!(self.start_locations.is_empty());
|
||||
FStringRanges { raw: self.raw }
|
||||
}
|
||||
}
|
|
@ -1,25 +1,26 @@
|
|||
//! Struct used to index source code, to enable efficient lookup of tokens that
|
||||
//! are omitted from the AST (e.g., commented lines).
|
||||
|
||||
use crate::CommentRangesBuilder;
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{StringKind, Tok};
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_python_trivia::{
|
||||
has_leading_content, has_trailing_content, is_python_whitespace, CommentRanges,
|
||||
};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::fstring_ranges::{FStringRanges, FStringRangesBuilder};
|
||||
use crate::CommentRangesBuilder;
|
||||
|
||||
pub struct Indexer {
|
||||
comment_ranges: CommentRanges,
|
||||
|
||||
/// Stores the start offset of continuation lines.
|
||||
continuation_lines: Vec<TextSize>,
|
||||
|
||||
/// The range of all f-string in the source document. The ranges are sorted by their
|
||||
/// [`TextRange::start`] position in increasing order. No two ranges are overlapping.
|
||||
f_string_ranges: Vec<TextRange>,
|
||||
/// The range of all f-string in the source document.
|
||||
fstring_ranges: FStringRanges,
|
||||
}
|
||||
|
||||
impl Indexer {
|
||||
|
@ -27,8 +28,8 @@ impl Indexer {
|
|||
assert!(TextSize::try_from(locator.contents().len()).is_ok());
|
||||
|
||||
let mut comment_ranges_builder = CommentRangesBuilder::default();
|
||||
let mut fstring_ranges_builder = FStringRangesBuilder::default();
|
||||
let mut continuation_lines = Vec::new();
|
||||
let mut f_string_ranges = Vec::new();
|
||||
// Token, end
|
||||
let mut prev_end = TextSize::default();
|
||||
let mut prev_token: Option<&Tok> = None;
|
||||
|
@ -59,18 +60,10 @@ impl Indexer {
|
|||
}
|
||||
|
||||
comment_ranges_builder.visit_token(tok, *range);
|
||||
fstring_ranges_builder.visit_token(tok, *range);
|
||||
|
||||
match tok {
|
||||
Tok::Newline | Tok::NonLogicalNewline => {
|
||||
line_start = range.end();
|
||||
}
|
||||
Tok::String {
|
||||
kind: StringKind::FString | StringKind::RawFString,
|
||||
..
|
||||
} => {
|
||||
f_string_ranges.push(*range);
|
||||
}
|
||||
_ => {}
|
||||
if matches!(tok, Tok::Newline | Tok::NonLogicalNewline) {
|
||||
line_start = range.end();
|
||||
}
|
||||
|
||||
prev_token = Some(tok);
|
||||
|
@ -79,7 +72,7 @@ impl Indexer {
|
|||
Self {
|
||||
comment_ranges: comment_ranges_builder.finish(),
|
||||
continuation_lines,
|
||||
f_string_ranges,
|
||||
fstring_ranges: fstring_ranges_builder.finish(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +81,11 @@ impl Indexer {
|
|||
&self.comment_ranges
|
||||
}
|
||||
|
||||
/// Returns the byte offset ranges of f-strings.
|
||||
pub const fn fstring_ranges(&self) -> &FStringRanges {
|
||||
&self.fstring_ranges
|
||||
}
|
||||
|
||||
/// Returns the line start positions of continuations (backslash).
|
||||
pub fn continuation_line_starts(&self) -> &[TextSize] {
|
||||
&self.continuation_lines
|
||||
|
@ -99,22 +97,6 @@ impl Indexer {
|
|||
self.continuation_lines.binary_search(&line_start).is_ok()
|
||||
}
|
||||
|
||||
/// Return the [`TextRange`] of the f-string containing a given offset.
|
||||
pub fn f_string_range(&self, offset: TextSize) -> Option<TextRange> {
|
||||
let Ok(string_range_index) = self.f_string_ranges.binary_search_by(|range| {
|
||||
if offset < range.start() {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if range.contains(offset) {
|
||||
std::cmp::Ordering::Equal
|
||||
} else {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
}) else {
|
||||
return None;
|
||||
};
|
||||
Some(self.f_string_ranges[string_range_index])
|
||||
}
|
||||
|
||||
/// Returns `true` if a statement or expression includes at least one comment.
|
||||
pub fn has_comments<T>(&self, node: &T, locator: &Locator) -> bool
|
||||
where
|
||||
|
@ -250,7 +232,7 @@ mod tests {
|
|||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{lexer, Mode};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::TextSize;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::Indexer;
|
||||
|
||||
|
@ -333,5 +315,203 @@ import os
|
|||
TextSize::from(116)
|
||||
]
|
||||
);
|
||||
|
||||
let contents = r"
|
||||
f'foo { 'str1' \
|
||||
'str2' \
|
||||
'str3'
|
||||
f'nested { 'str4'
|
||||
'str5' \
|
||||
'str6'
|
||||
}'
|
||||
}'
|
||||
"
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents));
|
||||
assert_eq!(
|
||||
indexer.continuation_line_starts(),
|
||||
[
|
||||
// row 1
|
||||
TextSize::new(0),
|
||||
// row 2
|
||||
TextSize::new(17),
|
||||
// row 5
|
||||
TextSize::new(63),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_f_string_ranges() {
|
||||
let contents = r#"
|
||||
f"normal f-string"
|
||||
f"start {f"inner {f"another"}"} end"
|
||||
f"implicit " f"concatenation"
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents));
|
||||
assert_eq!(
|
||||
indexer
|
||||
.fstring_ranges()
|
||||
.values()
|
||||
.copied()
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
TextRange::new(TextSize::from(0), TextSize::from(18)),
|
||||
TextRange::new(TextSize::from(19), TextSize::from(55)),
|
||||
TextRange::new(TextSize::from(28), TextSize::from(49)),
|
||||
TextRange::new(TextSize::from(37), TextSize::from(47)),
|
||||
TextRange::new(TextSize::from(56), TextSize::from(68)),
|
||||
TextRange::new(TextSize::from(69), TextSize::from(85)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_triple_quoted_f_string_ranges() {
|
||||
let contents = r#"
|
||||
f"""
|
||||
this is one
|
||||
multiline f-string
|
||||
"""
|
||||
f'''
|
||||
and this is
|
||||
another
|
||||
'''
|
||||
f"""
|
||||
this is a {f"""nested multiline
|
||||
f-string"""}
|
||||
"""
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents));
|
||||
assert_eq!(
|
||||
indexer
|
||||
.fstring_ranges()
|
||||
.values()
|
||||
.copied()
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
TextRange::new(TextSize::from(0), TextSize::from(39)),
|
||||
TextRange::new(TextSize::from(40), TextSize::from(68)),
|
||||
TextRange::new(TextSize::from(69), TextSize::from(122)),
|
||||
TextRange::new(TextSize::from(85), TextSize::from(117)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_innermost_outermost() {
|
||||
let contents = r#"
|
||||
f"no nested f-string"
|
||||
|
||||
if True:
|
||||
f"first {f"second {f"third"} second"} first"
|
||||
foo = "normal string"
|
||||
|
||||
f"implicit " f"concatenation"
|
||||
|
||||
f"first line {
|
||||
foo + f"second line {bar}"
|
||||
} third line"
|
||||
|
||||
f"""this is a
|
||||
multi-line {f"""nested
|
||||
f-string"""}
|
||||
the end"""
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents));
|
||||
|
||||
// For reference, the ranges of the f-strings in the above code are as
|
||||
// follows where the ones inside parentheses are nested f-strings:
|
||||
//
|
||||
// [0..21, (36..80, 45..72, 55..63), 108..120, 121..137, (139..198, 164..184), (200..260, 226..248)]
|
||||
|
||||
for (offset, innermost_range, outermost_range) in [
|
||||
// Inside a normal f-string
|
||||
(
|
||||
TextSize::new(130),
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
),
|
||||
// Left boundary
|
||||
(
|
||||
TextSize::new(121),
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
),
|
||||
// Right boundary
|
||||
(
|
||||
TextSize::new(136), // End offsets are exclusive
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
TextRange::new(TextSize::new(121), TextSize::new(137)),
|
||||
),
|
||||
// "first" left
|
||||
(
|
||||
TextSize::new(40),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
),
|
||||
// "second" left
|
||||
(
|
||||
TextSize::new(50),
|
||||
TextRange::new(TextSize::new(45), TextSize::new(72)),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
),
|
||||
// "third"
|
||||
(
|
||||
TextSize::new(60),
|
||||
TextRange::new(TextSize::new(55), TextSize::new(63)),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
),
|
||||
// "second" right
|
||||
(
|
||||
TextSize::new(70),
|
||||
TextRange::new(TextSize::new(45), TextSize::new(72)),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
),
|
||||
// "first" right
|
||||
(
|
||||
TextSize::new(75),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
TextRange::new(TextSize::new(36), TextSize::new(80)),
|
||||
),
|
||||
// Single-quoted f-strings spanning across multiple lines
|
||||
(
|
||||
TextSize::new(160),
|
||||
TextRange::new(TextSize::new(139), TextSize::new(198)),
|
||||
TextRange::new(TextSize::new(139), TextSize::new(198)),
|
||||
),
|
||||
(
|
||||
TextSize::new(170),
|
||||
TextRange::new(TextSize::new(164), TextSize::new(184)),
|
||||
TextRange::new(TextSize::new(139), TextSize::new(198)),
|
||||
),
|
||||
// Multi-line f-strings
|
||||
(
|
||||
TextSize::new(220),
|
||||
TextRange::new(TextSize::new(200), TextSize::new(260)),
|
||||
TextRange::new(TextSize::new(200), TextSize::new(260)),
|
||||
),
|
||||
(
|
||||
TextSize::new(240),
|
||||
TextRange::new(TextSize::new(226), TextSize::new(248)),
|
||||
TextRange::new(TextSize::new(200), TextSize::new(260)),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
indexer.fstring_ranges().innermost(offset).unwrap(),
|
||||
innermost_range
|
||||
);
|
||||
assert_eq!(
|
||||
indexer.fstring_ranges().outermost(offset).unwrap(),
|
||||
outermost_range
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod comment_ranges;
|
||||
mod fstring_ranges;
|
||||
mod indexer;
|
||||
|
||||
pub use comment_ranges::{tokens_and_ranges, CommentRangesBuilder};
|
||||
|
|
|
@ -18,6 +18,7 @@ ruff_python_ast = { path = "../ruff_python_ast" }
|
|||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
|
|
|
@ -37,6 +37,7 @@ use ruff_python_ast::{Int, IpyEscapeKind};
|
|||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::lexer::cursor::{Cursor, EOF_CHAR};
|
||||
use crate::lexer::fstring::{FStringContext, FStringContextFlags, FStrings};
|
||||
use crate::lexer::indentation::{Indentation, Indentations};
|
||||
use crate::{
|
||||
soft_keywords::SoftKeywordTransformer,
|
||||
|
@ -46,6 +47,7 @@ use crate::{
|
|||
};
|
||||
|
||||
mod cursor;
|
||||
mod fstring;
|
||||
mod indentation;
|
||||
|
||||
/// A lexer for Python source code.
|
||||
|
@ -62,6 +64,8 @@ pub struct Lexer<'source> {
|
|||
pending_indentation: Option<Indentation>,
|
||||
// Lexer mode.
|
||||
mode: Mode,
|
||||
// F-string contexts.
|
||||
fstrings: FStrings,
|
||||
}
|
||||
|
||||
/// Contains a Token along with its `range`.
|
||||
|
@ -154,6 +158,7 @@ impl<'source> Lexer<'source> {
|
|||
source: input,
|
||||
cursor: Cursor::new(input),
|
||||
mode,
|
||||
fstrings: FStrings::default(),
|
||||
};
|
||||
// TODO: Handle possible mismatch between BOM and explicit encoding declaration.
|
||||
// spell-checker:ignore feff
|
||||
|
@ -165,16 +170,24 @@ impl<'source> Lexer<'source> {
|
|||
/// Lex an identifier. Also used for keywords and string/bytes literals with a prefix.
|
||||
fn lex_identifier(&mut self, first: char) -> Result<Tok, LexicalError> {
|
||||
// Detect potential string like rb'' b'' f'' u'' r''
|
||||
match self.cursor.first() {
|
||||
quote @ ('\'' | '"') => {
|
||||
match (first, self.cursor.first()) {
|
||||
('f' | 'F', quote @ ('\'' | '"')) => {
|
||||
self.cursor.bump();
|
||||
return Ok(self.lex_fstring_start(quote, false));
|
||||
}
|
||||
('r' | 'R', 'f' | 'F') | ('f' | 'F', 'r' | 'R') if is_quote(self.cursor.second()) => {
|
||||
self.cursor.bump();
|
||||
let quote = self.cursor.bump().unwrap();
|
||||
return Ok(self.lex_fstring_start(quote, true));
|
||||
}
|
||||
(_, quote @ ('\'' | '"')) => {
|
||||
if let Ok(string_kind) = StringKind::try_from(first) {
|
||||
self.cursor.bump();
|
||||
return self.lex_string(string_kind, quote);
|
||||
}
|
||||
}
|
||||
second @ ('f' | 'F' | 'r' | 'R' | 'b' | 'B') if is_quote(self.cursor.second()) => {
|
||||
(_, second @ ('r' | 'R' | 'b' | 'B')) if is_quote(self.cursor.second()) => {
|
||||
self.cursor.bump();
|
||||
|
||||
if let Ok(string_kind) = StringKind::try_from([first, second]) {
|
||||
let quote = self.cursor.bump().unwrap();
|
||||
return self.lex_string(string_kind, quote);
|
||||
|
@ -509,6 +522,148 @@ impl<'source> Lexer<'source> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Lex a f-string start token.
|
||||
fn lex_fstring_start(&mut self, quote: char, is_raw_string: bool) -> Tok {
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert_eq!(self.cursor.previous(), quote);
|
||||
|
||||
let mut flags = FStringContextFlags::empty();
|
||||
if quote == '"' {
|
||||
flags |= FStringContextFlags::DOUBLE;
|
||||
}
|
||||
if is_raw_string {
|
||||
flags |= FStringContextFlags::RAW;
|
||||
}
|
||||
if self.cursor.eat_char2(quote, quote) {
|
||||
flags |= FStringContextFlags::TRIPLE;
|
||||
}
|
||||
|
||||
self.fstrings.push(FStringContext::new(flags, self.nesting));
|
||||
Tok::FStringStart
|
||||
}
|
||||
|
||||
/// Lex a f-string middle or end token.
|
||||
fn lex_fstring_middle_or_end(&mut self) -> Result<Option<Tok>, LexicalError> {
|
||||
// SAFETY: Safe because the function is only called when `self.fstrings` is not empty.
|
||||
let fstring = self.fstrings.current().unwrap();
|
||||
self.cursor.start_token();
|
||||
|
||||
// Check if we're at the end of the f-string.
|
||||
if fstring.is_triple_quoted() {
|
||||
let quote_char = fstring.quote_char();
|
||||
if self.cursor.eat_char3(quote_char, quote_char, quote_char) {
|
||||
return Ok(Some(Tok::FStringEnd));
|
||||
}
|
||||
} else if self.cursor.eat_char(fstring.quote_char()) {
|
||||
return Ok(Some(Tok::FStringEnd));
|
||||
}
|
||||
|
||||
// We have to decode `{{` and `}}` into `{` and `}` respectively. As an
|
||||
// optimization, we only allocate a new string we find any escaped curly braces,
|
||||
// otherwise this string will remain empty and we'll use a source slice instead.
|
||||
let mut normalized = String::new();
|
||||
|
||||
// Tracks the last offset of token value that has been written to `normalized`.
|
||||
let mut last_offset = self.offset();
|
||||
|
||||
let mut in_named_unicode = false;
|
||||
|
||||
loop {
|
||||
match self.cursor.first() {
|
||||
// The condition is to differentiate between the `NUL` (`\0`) character
|
||||
// in the source code and the one returned by `self.cursor.first()` when
|
||||
// we reach the end of the source code.
|
||||
EOF_CHAR if self.cursor.is_eof() => {
|
||||
let error = if fstring.is_triple_quoted() {
|
||||
FStringErrorType::UnterminatedTripleQuotedString
|
||||
} else {
|
||||
FStringErrorType::UnterminatedString
|
||||
};
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(error),
|
||||
location: self.offset(),
|
||||
});
|
||||
}
|
||||
'\n' if !fstring.is_triple_quoted() => {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(FStringErrorType::UnterminatedString),
|
||||
location: self.offset(),
|
||||
});
|
||||
}
|
||||
'\\' => {
|
||||
self.cursor.bump(); // '\'
|
||||
if matches!(self.cursor.first(), '{' | '}') {
|
||||
// Don't consume `{` or `}` as we want them to be emitted as tokens.
|
||||
// They will be handled in the next iteration.
|
||||
continue;
|
||||
} else if !fstring.is_raw_string() {
|
||||
if self.cursor.eat_char2('N', '{') {
|
||||
in_named_unicode = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Consume the escaped character.
|
||||
self.cursor.bump();
|
||||
}
|
||||
quote @ ('\'' | '"') if quote == fstring.quote_char() => {
|
||||
if let Some(triple_quotes) = fstring.triple_quotes() {
|
||||
if self.cursor.rest().starts_with(triple_quotes) {
|
||||
break;
|
||||
}
|
||||
self.cursor.bump();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
'{' => {
|
||||
if self.cursor.second() == '{' {
|
||||
self.cursor.bump();
|
||||
normalized
|
||||
.push_str(&self.source[TextRange::new(last_offset, self.offset())]);
|
||||
self.cursor.bump(); // Skip the second `{`
|
||||
last_offset = self.offset();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
'}' => {
|
||||
if in_named_unicode {
|
||||
in_named_unicode = false;
|
||||
self.cursor.bump();
|
||||
} else if self.cursor.second() == '}'
|
||||
&& !fstring.is_in_format_spec(self.nesting)
|
||||
{
|
||||
self.cursor.bump();
|
||||
normalized
|
||||
.push_str(&self.source[TextRange::new(last_offset, self.offset())]);
|
||||
self.cursor.bump(); // Skip the second `}`
|
||||
last_offset = self.offset();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.cursor.bump();
|
||||
}
|
||||
}
|
||||
}
|
||||
let range = self.token_range();
|
||||
if range.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let value = if normalized.is_empty() {
|
||||
self.source[range].to_string()
|
||||
} else {
|
||||
normalized.push_str(&self.source[TextRange::new(last_offset, self.offset())]);
|
||||
normalized
|
||||
};
|
||||
Ok(Some(Tok::FStringMiddle {
|
||||
value,
|
||||
is_raw: fstring.is_raw_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Lex a string literal.
|
||||
fn lex_string(&mut self, kind: StringKind, quote: char) -> Result<Tok, LexicalError> {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -530,6 +685,19 @@ impl<'source> Lexer<'source> {
|
|||
}
|
||||
}
|
||||
Some('\r' | '\n') if !triple_quoted => {
|
||||
if let Some(fstring) = self.fstrings.current() {
|
||||
// When we are in an f-string, check whether does the initial quote
|
||||
// matches with f-strings quotes and if it is, then this must be a
|
||||
// missing '}' token so raise the proper error.
|
||||
if fstring.quote_char() == quote && !fstring.is_triple_quoted() {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(
|
||||
FStringErrorType::UnclosedLbrace,
|
||||
),
|
||||
location: self.offset() - fstring.quote_size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::OtherError(
|
||||
"EOL while scanning string literal".to_owned(),
|
||||
|
@ -549,6 +717,21 @@ impl<'source> Lexer<'source> {
|
|||
|
||||
Some(_) => {}
|
||||
None => {
|
||||
if let Some(fstring) = self.fstrings.current() {
|
||||
// When we are in an f-string, check whether does the initial quote
|
||||
// matches with f-strings quotes and if it is, then this must be a
|
||||
// missing '}' token so raise the proper error.
|
||||
if fstring.quote_char() == quote
|
||||
&& fstring.is_triple_quoted() == triple_quoted
|
||||
{
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(
|
||||
FStringErrorType::UnclosedLbrace,
|
||||
),
|
||||
location: self.offset() - fstring.quote_size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return Err(LexicalError {
|
||||
error: if triple_quoted {
|
||||
LexicalErrorType::Eof
|
||||
|
@ -572,8 +755,28 @@ impl<'source> Lexer<'source> {
|
|||
// This is the main entry point. Call this function to retrieve the next token.
|
||||
// This function is used by the iterator implementation.
|
||||
pub fn next_token(&mut self) -> LexResult {
|
||||
if let Some(fstring) = self.fstrings.current() {
|
||||
if !fstring.is_in_expression(self.nesting) {
|
||||
match self.lex_fstring_middle_or_end() {
|
||||
Ok(Some(tok)) => {
|
||||
if tok == Tok::FStringEnd {
|
||||
self.fstrings.pop();
|
||||
}
|
||||
return Ok((tok, self.token_range()));
|
||||
}
|
||||
Err(e) => {
|
||||
// This is to prevent an infinite loop in which the lexer
|
||||
// continuously returns an error token because the f-string
|
||||
// remains on the stack.
|
||||
self.fstrings.pop();
|
||||
return Err(e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return dedent tokens until the current indentation level matches the indentation of the next token.
|
||||
if let Some(indentation) = self.pending_indentation.take() {
|
||||
else if let Some(indentation) = self.pending_indentation.take() {
|
||||
match self.indentations.current().try_compare(indentation) {
|
||||
Ok(Ordering::Greater) => {
|
||||
self.pending_indentation = Some(indentation);
|
||||
|
@ -894,10 +1097,7 @@ impl<'source> Lexer<'source> {
|
|||
if self.cursor.eat_char('=') {
|
||||
Tok::NotEqual
|
||||
} else {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::UnrecognizedToken { tok: '!' },
|
||||
location: self.token_start(),
|
||||
});
|
||||
Tok::Exclamation
|
||||
}
|
||||
}
|
||||
'~' => Tok::Tilde,
|
||||
|
@ -922,11 +1122,26 @@ impl<'source> Lexer<'source> {
|
|||
Tok::Lbrace
|
||||
}
|
||||
'}' => {
|
||||
if let Some(fstring) = self.fstrings.current_mut() {
|
||||
if fstring.nesting() == self.nesting {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(FStringErrorType::SingleRbrace),
|
||||
location: self.token_start(),
|
||||
});
|
||||
}
|
||||
fstring.try_end_format_spec(self.nesting);
|
||||
}
|
||||
self.nesting = self.nesting.saturating_sub(1);
|
||||
Tok::Rbrace
|
||||
}
|
||||
':' => {
|
||||
if self.cursor.eat_char('=') {
|
||||
if self
|
||||
.fstrings
|
||||
.current_mut()
|
||||
.is_some_and(|fstring| fstring.try_start_format_spec(self.nesting))
|
||||
{
|
||||
Tok::Colon
|
||||
} else if self.cursor.eat_char('=') {
|
||||
Tok::ColonEqual
|
||||
} else {
|
||||
Tok::Colon
|
||||
|
@ -1743,4 +1958,191 @@ def f(arg=%timeit a = b):
|
|||
.collect();
|
||||
assert_debug_snapshot!(tokens);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_fstrings() {
|
||||
let source = r#"f"" "" F"" f'' '' f"""""" f''''''"#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_prefix() {
|
||||
let source = r#"f"" F"" rf"" rF"" Rf"" RF"" fr"" Fr"" fR"" FR"""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring() {
|
||||
let source = r#"f"normal {foo} {{another}} {bar} {{{three}}}""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_parentheses() {
|
||||
let source = r#"f"{}" f"{{}}" f" {}" f"{{{}}}" f"{{{{}}}}" f" {} {{}} {{{}}} {{{{}}}} ""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_escape() {
|
||||
let source = r#"f"\{x:\"\{x}} \"\"\
|
||||
end""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_escape_braces() {
|
||||
let source = r"f'\{foo}' f'\\{foo}' f'\{{foo}}' f'\\{{foo}}'";
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_escape_raw() {
|
||||
let source = r#"rf"\{x:\"\{x}} \"\"\
|
||||
end""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_named_unicode() {
|
||||
let source = r#"f"\N{BULLET} normal \Nope \N""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_named_unicode_raw() {
|
||||
let source = r#"rf"\N{BULLET} normal""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_with_named_expression() {
|
||||
let source = r#"f"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_with_format_spec() {
|
||||
let source = r#"f"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}}""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_conversion() {
|
||||
let source = r#"f"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_nested() {
|
||||
let source = r#"f"foo {f"bar {x + f"{wow}"}"} baz" f'foo {f'bar'} some {f"another"}'"#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_expression_multiline() {
|
||||
let source = r#"f"first {
|
||||
x
|
||||
*
|
||||
y
|
||||
} second""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_multiline() {
|
||||
let source = r#"f"""
|
||||
hello
|
||||
world
|
||||
""" f'''
|
||||
world
|
||||
hello
|
||||
''' f"some {f"""multiline
|
||||
allowed {x}"""} string""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_comments() {
|
||||
let source = r#"f"""
|
||||
# not a comment { # comment {
|
||||
x
|
||||
} # not a comment
|
||||
""""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_with_ipy_escape_command() {
|
||||
let source = r#"f"foo {!pwd} bar""#;
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_with_lambda_expression() {
|
||||
let source = r#"
|
||||
f"{lambda x:{x}}"
|
||||
f"{(lambda x:{x})}"
|
||||
"#
|
||||
.trim();
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_with_nul_char() {
|
||||
let source = r"f'\0'";
|
||||
assert_debug_snapshot!(lex_source(source));
|
||||
}
|
||||
|
||||
fn lex_fstring_error(source: &str) -> FStringErrorType {
|
||||
match lex(source, Mode::Module).find_map(std::result::Result::err) {
|
||||
Some(err) => match err.error {
|
||||
LexicalErrorType::FStringError(error) => error,
|
||||
_ => panic!("Expected FStringError: {err:?}"),
|
||||
},
|
||||
_ => panic!("Expected atleast one FStringError"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstring_error() {
|
||||
use FStringErrorType::{
|
||||
SingleRbrace, UnclosedLbrace, UnterminatedString, UnterminatedTripleQuotedString,
|
||||
};
|
||||
|
||||
assert_eq!(lex_fstring_error("f'}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error("f'{{}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error("f'{{}}}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error("f'foo}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error(r"f'\u007b}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error("f'{a:b}}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error("f'{3:}}>10}'"), SingleRbrace);
|
||||
assert_eq!(lex_fstring_error(r"f'\{foo}\}'"), SingleRbrace);
|
||||
|
||||
assert_eq!(lex_fstring_error("f'{'"), UnclosedLbrace);
|
||||
assert_eq!(lex_fstring_error("f'{foo!r'"), UnclosedLbrace);
|
||||
assert_eq!(lex_fstring_error("f'{foo='"), UnclosedLbrace);
|
||||
assert_eq!(
|
||||
lex_fstring_error(
|
||||
r#"f"{"
|
||||
"#
|
||||
),
|
||||
UnclosedLbrace
|
||||
);
|
||||
assert_eq!(lex_fstring_error(r#"f"""{""""#), UnclosedLbrace);
|
||||
|
||||
assert_eq!(lex_fstring_error(r#"f""#), UnterminatedString);
|
||||
assert_eq!(lex_fstring_error(r#"f'"#), UnterminatedString);
|
||||
|
||||
assert_eq!(lex_fstring_error(r#"f""""#), UnterminatedTripleQuotedString);
|
||||
assert_eq!(lex_fstring_error(r#"f'''"#), UnterminatedTripleQuotedString);
|
||||
assert_eq!(
|
||||
lex_fstring_error(r#"f"""""#),
|
||||
UnterminatedTripleQuotedString
|
||||
);
|
||||
assert_eq!(
|
||||
lex_fstring_error(r#"f""""""#),
|
||||
UnterminatedTripleQuotedString
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,18 @@ impl<'a> Cursor<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn eat_char3(&mut self, c1: char, c2: char, c3: char) -> bool {
|
||||
let mut chars = self.chars.clone();
|
||||
if chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3) {
|
||||
self.bump();
|
||||
self.bump();
|
||||
self.bump();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn eat_if<F>(&mut self, mut predicate: F) -> Option<char>
|
||||
where
|
||||
F: FnMut(char) -> bool,
|
||||
|
|
161
crates/ruff_python_parser/src/lexer/fstring.rs
Normal file
161
crates/ruff_python_parser/src/lexer/fstring.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use bitflags::bitflags;
|
||||
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FStringContextFlags: u8 {
|
||||
/// The current f-string is a triple-quoted f-string i.e., the number of
|
||||
/// opening quotes is 3. If this flag is not set, the number of opening
|
||||
/// quotes is 1.
|
||||
const TRIPLE = 1 << 0;
|
||||
|
||||
/// The current f-string is a double-quoted f-string. If this flag is not
|
||||
/// set, the current f-string is a single-quoted f-string.
|
||||
const DOUBLE = 1 << 1;
|
||||
|
||||
/// The current f-string is a raw f-string i.e., prefixed with `r`/`R`.
|
||||
/// If this flag is not set, the current f-string is a normal f-string.
|
||||
const RAW = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// The context representing the current f-string that the lexer is in.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FStringContext {
|
||||
flags: FStringContextFlags,
|
||||
|
||||
/// The level of nesting for the lexer when it entered the current f-string.
|
||||
/// The nesting level includes all kinds of parentheses i.e., round, square,
|
||||
/// and curly.
|
||||
nesting: u32,
|
||||
|
||||
/// The current depth of format spec for the current f-string. This is because
|
||||
/// there can be multiple format specs nested for the same f-string.
|
||||
/// For example, `{a:{b:{c}}}` has 3 format specs.
|
||||
format_spec_depth: u32,
|
||||
}
|
||||
|
||||
impl FStringContext {
|
||||
pub(crate) const fn new(flags: FStringContextFlags, nesting: u32) -> Self {
|
||||
Self {
|
||||
flags,
|
||||
nesting,
|
||||
format_spec_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn nesting(&self) -> u32 {
|
||||
self.nesting
|
||||
}
|
||||
|
||||
/// Returns the quote character for the current f-string.
|
||||
pub(crate) const fn quote_char(&self) -> char {
|
||||
if self.flags.contains(FStringContextFlags::DOUBLE) {
|
||||
'"'
|
||||
} else {
|
||||
'\''
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of quotes for the current f-string.
|
||||
pub(crate) const fn quote_size(&self) -> TextSize {
|
||||
if self.is_triple_quoted() {
|
||||
TextSize::new(3)
|
||||
} else {
|
||||
TextSize::new(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the triple quotes for the current f-string if it is a triple-quoted
|
||||
/// f-string, `None` otherwise.
|
||||
pub(crate) const fn triple_quotes(&self) -> Option<&'static str> {
|
||||
if self.is_triple_quoted() {
|
||||
if self.flags.contains(FStringContextFlags::DOUBLE) {
|
||||
Some(r#"""""#)
|
||||
} else {
|
||||
Some("'''")
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the current f-string is a raw f-string.
|
||||
pub(crate) const fn is_raw_string(&self) -> bool {
|
||||
self.flags.contains(FStringContextFlags::RAW)
|
||||
}
|
||||
|
||||
/// Returns `true` if the current f-string is a triple-quoted f-string.
|
||||
pub(crate) const fn is_triple_quoted(&self) -> bool {
|
||||
self.flags.contains(FStringContextFlags::TRIPLE)
|
||||
}
|
||||
|
||||
/// Calculates the number of open parentheses for the current f-string
|
||||
/// based on the current level of nesting for the lexer.
|
||||
const fn open_parentheses_count(&self, current_nesting: u32) -> u32 {
|
||||
current_nesting.saturating_sub(self.nesting)
|
||||
}
|
||||
|
||||
/// Returns `true` if the lexer is in a f-string expression i.e., between
|
||||
/// two curly braces.
|
||||
pub(crate) const fn is_in_expression(&self, current_nesting: u32) -> bool {
|
||||
self.open_parentheses_count(current_nesting) > self.format_spec_depth
|
||||
}
|
||||
|
||||
/// Returns `true` if the lexer is in a f-string format spec i.e., after a colon.
|
||||
pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool {
|
||||
self.format_spec_depth > 0 && !self.is_in_expression(current_nesting)
|
||||
}
|
||||
|
||||
/// Returns `true` if the context is in a valid position to start format spec
|
||||
/// i.e., at the same level of nesting as the opening parentheses token.
|
||||
/// Increments the format spec depth if it is.
|
||||
///
|
||||
/// This assumes that the current character for the lexer is a colon (`:`).
|
||||
pub(crate) fn try_start_format_spec(&mut self, current_nesting: u32) -> bool {
|
||||
if self
|
||||
.open_parentheses_count(current_nesting)
|
||||
.saturating_sub(self.format_spec_depth)
|
||||
== 1
|
||||
{
|
||||
self.format_spec_depth += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrements the format spec depth if the current f-string is in a format
|
||||
/// spec.
|
||||
pub(crate) fn try_end_format_spec(&mut self, current_nesting: u32) {
|
||||
if self.is_in_format_spec(current_nesting) {
|
||||
self.format_spec_depth = self.format_spec_depth.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The f-strings stack is used to keep track of all the f-strings that the
|
||||
/// lexer encounters. This is necessary because f-strings can be nested.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct FStrings {
|
||||
stack: Vec<FStringContext>,
|
||||
}
|
||||
|
||||
impl FStrings {
|
||||
pub(crate) fn push(&mut self, context: FStringContext) {
|
||||
self.stack.push(context);
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) -> Option<FStringContext> {
|
||||
self.stack.pop()
|
||||
}
|
||||
|
||||
pub(crate) fn current(&self) -> Option<&FStringContext> {
|
||||
self.stack.last()
|
||||
}
|
||||
|
||||
pub(crate) fn current_mut(&mut self) -> Option<&mut FStringContext> {
|
||||
self.stack.last_mut()
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@
|
|||
//! return bool(i & 1)
|
||||
//! "#;
|
||||
//! let tokens = lex(python_source, Mode::Module);
|
||||
//! let ast = parse_tokens(tokens, Mode::Module, "<embedded>");
|
||||
//! let ast = parse_tokens(tokens, python_source, Mode::Module, "<embedded>");
|
||||
//!
|
||||
//! assert!(ast.is_ok());
|
||||
//! ```
|
||||
|
@ -146,6 +146,7 @@ pub fn tokenize(contents: &str, mode: Mode) -> Vec<LexResult> {
|
|||
/// Parse a full Python program from its tokens.
|
||||
pub fn parse_program_tokens(
|
||||
lxr: Vec<LexResult>,
|
||||
source: &str,
|
||||
source_path: &str,
|
||||
is_jupyter_notebook: bool,
|
||||
) -> anyhow::Result<Suite, ParseError> {
|
||||
|
@ -154,7 +155,7 @@ pub fn parse_program_tokens(
|
|||
} else {
|
||||
Mode::Module
|
||||
};
|
||||
match parse_tokens(lxr, mode, source_path)? {
|
||||
match parse_tokens(lxr, source, mode, source_path)? {
|
||||
Mod::Module(m) => Ok(m.body),
|
||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ use ruff_python_ast::{Mod, ModModule, Suite};
|
|||
/// ```
|
||||
pub fn parse_program(source: &str, source_path: &str) -> Result<ModModule, ParseError> {
|
||||
let lexer = lex(source, Mode::Module);
|
||||
match parse_tokens(lexer, Mode::Module, source_path)? {
|
||||
match parse_tokens(lexer, source, Mode::Module, source_path)? {
|
||||
Mod::Module(m) => Ok(m),
|
||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ pub fn parse_suite(source: &str, source_path: &str) -> Result<Suite, ParseError>
|
|||
/// ```
|
||||
pub fn parse_expression(source: &str, source_path: &str) -> Result<ast::Expr, ParseError> {
|
||||
let lexer = lex(source, Mode::Expression);
|
||||
match parse_tokens(lexer, Mode::Expression, source_path)? {
|
||||
match parse_tokens(lexer, source, Mode::Expression, source_path)? {
|
||||
Mod::Expression(expression) => Ok(*expression.body),
|
||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ pub fn parse_expression_starts_at(
|
|||
offset: TextSize,
|
||||
) -> Result<ast::Expr, ParseError> {
|
||||
let lexer = lex_starts_at(source, Mode::Module, offset);
|
||||
match parse_tokens(lexer, Mode::Expression, source_path)? {
|
||||
match parse_tokens(lexer, source, Mode::Expression, source_path)? {
|
||||
Mod::Expression(expression) => Ok(*expression.body),
|
||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ pub fn parse_starts_at(
|
|||
offset: TextSize,
|
||||
) -> Result<Mod, ParseError> {
|
||||
let lxr = lexer::lex_starts_at(source, mode, offset);
|
||||
parse_tokens(lxr, mode, source_path)
|
||||
parse_tokens(lxr, source, mode, source_path)
|
||||
}
|
||||
|
||||
/// Parse an iterator of [`LexResult`]s using the specified [`Mode`].
|
||||
|
@ -208,11 +208,13 @@ pub fn parse_starts_at(
|
|||
/// ```
|
||||
/// use ruff_python_parser::{lexer::lex, Mode, parse_tokens};
|
||||
///
|
||||
/// let expr = parse_tokens(lex("1 + 2", Mode::Expression), Mode::Expression, "<embedded>");
|
||||
/// let source = "1 + 2";
|
||||
/// let expr = parse_tokens(lex(source, Mode::Expression), source, Mode::Expression, "<embedded>");
|
||||
/// assert!(expr.is_ok());
|
||||
/// ```
|
||||
pub fn parse_tokens(
|
||||
lxr: impl IntoIterator<Item = LexResult>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
source_path: &str,
|
||||
) -> Result<Mod, ParseError> {
|
||||
|
@ -220,6 +222,7 @@ pub fn parse_tokens(
|
|||
|
||||
parse_filtered_tokens(
|
||||
lxr.filter_ok(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline)),
|
||||
source,
|
||||
mode,
|
||||
source_path,
|
||||
)
|
||||
|
@ -228,6 +231,7 @@ pub fn parse_tokens(
|
|||
/// Parse tokens into an AST like [`parse_tokens`], but we already know all tokens are valid.
|
||||
pub fn parse_ok_tokens(
|
||||
lxr: impl IntoIterator<Item = Spanned>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
source_path: &str,
|
||||
) -> Result<Mod, ParseError> {
|
||||
|
@ -239,12 +243,13 @@ pub fn parse_ok_tokens(
|
|||
.chain(lxr)
|
||||
.map(|(t, range)| (range.start(), t, range.end()));
|
||||
python::TopParser::new()
|
||||
.parse(mode, lexer)
|
||||
.parse(source, mode, lexer)
|
||||
.map_err(|e| parse_error_from_lalrpop(e, source_path))
|
||||
}
|
||||
|
||||
fn parse_filtered_tokens(
|
||||
lxr: impl IntoIterator<Item = LexResult>,
|
||||
source: &str,
|
||||
mode: Mode,
|
||||
source_path: &str,
|
||||
) -> Result<Mod, ParseError> {
|
||||
|
@ -252,6 +257,7 @@ fn parse_filtered_tokens(
|
|||
let lexer = iter::once(Ok(marker_token)).chain(lxr);
|
||||
python::TopParser::new()
|
||||
.parse(
|
||||
source,
|
||||
mode,
|
||||
lexer.map_ok(|(t, range)| (range.start(), t, range.end())),
|
||||
)
|
||||
|
@ -1253,11 +1259,58 @@ a = 1
|
|||
"#
|
||||
.trim();
|
||||
let lxr = lexer::lex_starts_at(source, Mode::Ipython, TextSize::default());
|
||||
let parse_err = parse_tokens(lxr, Mode::Module, "<test>").unwrap_err();
|
||||
let parse_err = parse_tokens(lxr, source, Mode::Module, "<test>").unwrap_err();
|
||||
assert_eq!(
|
||||
parse_err.to_string(),
|
||||
"IPython escape commands are only allowed in `Mode::Ipython` at byte offset 6"
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstrings() {
|
||||
let parse_ast = parse_suite(
|
||||
r#"
|
||||
f"{" f"}"
|
||||
f"{foo!s}"
|
||||
f"{3,}"
|
||||
f"{3!=4:}"
|
||||
f'{3:{"}"}>10}'
|
||||
f'{3:{"{"}>10}'
|
||||
f"{ foo = }"
|
||||
f"{ foo = :.3f }"
|
||||
f"{ foo = !s }"
|
||||
f"{ 1, 2 = }"
|
||||
f'{f"{3.1415=:.1f}":*^20}'
|
||||
|
||||
{"foo " f"bar {x + y} " "baz": 10}
|
||||
match foo:
|
||||
case "foo " f"bar {x + y} " "baz":
|
||||
pass
|
||||
|
||||
f"\{foo}\{bar:\}"
|
||||
f"\\{{foo\\}}"
|
||||
"#
|
||||
.trim(),
|
||||
"<test>",
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(parse_ast);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fstrings_with_unicode() {
|
||||
let parse_ast = parse_suite(
|
||||
r#"
|
||||
u"foo" f"{bar}" "baz" " some"
|
||||
"foo" f"{bar}" u"baz" " some"
|
||||
"foo" f"{bar}" "baz" u" some"
|
||||
u"foo" f"bar {baz} really" u"bar" "no"
|
||||
"#
|
||||
.trim(),
|
||||
"<test>",
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(parse_ast);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
// See also: file:///usr/share/doc/python/html/reference/compound_stmts.html#function-definitions
|
||||
// See also: https://greentreesnakes.readthedocs.io/en/latest/nodes.html#keyword
|
||||
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ruff_python_ast::{self as ast, Int, IpyEscapeKind};
|
||||
use crate::{
|
||||
FStringErrorType,
|
||||
Mode,
|
||||
lexer::{LexicalError, LexicalErrorType},
|
||||
function::{ArgumentList, parse_arguments, validate_pos_params, validate_arguments},
|
||||
context::set_context,
|
||||
string::parse_strings,
|
||||
string::{StringType, concatenate_strings, parse_fstring_middle, parse_string_literal},
|
||||
token::{self, StringKind},
|
||||
};
|
||||
use lalrpop_util::ParseError;
|
||||
|
||||
grammar(mode: Mode);
|
||||
grammar(source_code: &str, mode: Mode);
|
||||
|
||||
// This is a hack to reduce the amount of lalrpop tables generated:
|
||||
// For each public entry point, a full parse table is generated.
|
||||
|
@ -667,8 +668,8 @@ LiteralPattern: ast::Pattern = {
|
|||
value: Box::new(value.into()),
|
||||
range: (location..end_location).into()
|
||||
}.into(),
|
||||
<location:@L> <s:(@L string @R)+> <end_location:@R> =>? Ok(ast::PatternMatchValue {
|
||||
value: Box::new(parse_strings(s)?),
|
||||
<location:@L> <strings:StringLiteralOrFString+> <end_location:@R> =>? Ok(ast::PatternMatchValue {
|
||||
value: Box::new(concatenate_strings(strings, (location..end_location).into())?),
|
||||
range: (location..end_location).into()
|
||||
}.into()),
|
||||
}
|
||||
|
@ -725,7 +726,7 @@ MappingKey: ast::Expr = {
|
|||
value: false.into(),
|
||||
range: (location..end_location).into()
|
||||
}.into(),
|
||||
<location:@L> <s:(@L string @R)+> =>? Ok(parse_strings(s)?),
|
||||
<location:@L> <strings:StringLiteralOrFString+> <end_location:@R> =>? Ok(concatenate_strings(strings, (location..end_location).into())?),
|
||||
}
|
||||
|
||||
MatchMappingEntry: (ast::Expr, ast::Pattern) = {
|
||||
|
@ -1349,7 +1350,13 @@ NamedExpression: ast::ParenthesizedExpr = {
|
|||
};
|
||||
|
||||
LambdaDef: ast::ParenthesizedExpr = {
|
||||
<location:@L> "lambda" <location_args:@L> <parameters:ParameterList<UntypedParameter, StarUntypedParameter, StarUntypedParameter>?> <end_location_args:@R> ":" <body:Test<"all">> <end_location:@R> =>? {
|
||||
<location:@L> "lambda" <location_args:@L> <parameters:ParameterList<UntypedParameter, StarUntypedParameter, StarUntypedParameter>?> <end_location_args:@R> ":" <fstring_middle:fstring_middle?> <body:Test<"all">> <end_location:@R> =>? {
|
||||
if fstring_middle.is_some() {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses),
|
||||
location,
|
||||
})?;
|
||||
}
|
||||
parameters.as_ref().map(validate_arguments).transpose()?;
|
||||
|
||||
Ok(ast::ExprLambda {
|
||||
|
@ -1572,8 +1579,105 @@ SliceOp: Option<ast::ParenthesizedExpr> = {
|
|||
<location:@L> ":" <e:Test<"all">?> => e,
|
||||
}
|
||||
|
||||
StringLiteralOrFString: StringType = {
|
||||
StringLiteral,
|
||||
FStringExpr,
|
||||
};
|
||||
|
||||
StringLiteral: StringType = {
|
||||
<start_location:@L> <string:string> =>? {
|
||||
let (source, kind, triple_quoted) = string;
|
||||
Ok(parse_string_literal(&source, kind, triple_quoted, start_location)?)
|
||||
}
|
||||
};
|
||||
|
||||
FStringExpr: StringType = {
|
||||
<location:@L> FStringStart <values:FStringMiddlePattern*> FStringEnd <end_location:@R> => {
|
||||
StringType::FString(ast::ExprFString {
|
||||
values,
|
||||
implicit_concatenated: false,
|
||||
range: (location..end_location).into()
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
FStringMiddlePattern: ast::Expr = {
|
||||
FStringReplacementField,
|
||||
<start_location:@L> <fstring_middle:fstring_middle> =>? {
|
||||
let (source, is_raw) = fstring_middle;
|
||||
Ok(parse_fstring_middle(&source, is_raw, start_location)?)
|
||||
}
|
||||
};
|
||||
|
||||
FStringReplacementField: ast::Expr = {
|
||||
<location:@L> "{" <value:TestListOrYieldExpr> <debug:"="?> <conversion:FStringConversion?> <format_spec:FStringFormatSpecSuffix?> "}" <end_location:@R> =>? {
|
||||
if value.expr.is_lambda_expr() && !value.is_parenthesized() {
|
||||
return Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses),
|
||||
location: value.start(),
|
||||
})?;
|
||||
}
|
||||
let debug_text = debug.map(|_| {
|
||||
let start_offset = location + "{".text_len();
|
||||
let end_offset = if let Some((conversion_start, _)) = conversion {
|
||||
conversion_start
|
||||
} else {
|
||||
format_spec.as_ref().map_or_else(
|
||||
|| end_location - "}".text_len(),
|
||||
|spec| spec.range().start() - ":".text_len(),
|
||||
)
|
||||
};
|
||||
ast::DebugText {
|
||||
leading: source_code[TextRange::new(start_offset, value.range().start())].to_string(),
|
||||
trailing: source_code[TextRange::new(value.range().end(), end_offset)].to_string(),
|
||||
}
|
||||
});
|
||||
Ok(
|
||||
ast::ExprFormattedValue {
|
||||
value: Box::new(value.into()),
|
||||
debug_text,
|
||||
conversion: conversion.map_or(ast::ConversionFlag::None, |(_, conversion_flag)| {
|
||||
conversion_flag
|
||||
}),
|
||||
format_spec: format_spec.map(Box::new),
|
||||
range: (location..end_location).into(),
|
||||
}
|
||||
.into()
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
FStringFormatSpecSuffix: ast::Expr = {
|
||||
":" <format_spec:FStringFormatSpec> => format_spec
|
||||
};
|
||||
|
||||
FStringFormatSpec: ast::Expr = {
|
||||
<location:@L> <values:FStringMiddlePattern*> <end_location:@R> => {
|
||||
ast::ExprFString {
|
||||
values,
|
||||
implicit_concatenated: false,
|
||||
range: (location..end_location).into()
|
||||
}.into()
|
||||
},
|
||||
};
|
||||
|
||||
FStringConversion: (TextSize, ast::ConversionFlag) = {
|
||||
<location:@L> "!" <s:name> =>? {
|
||||
let conversion = match s.as_str() {
|
||||
"s" => ast::ConversionFlag::Str,
|
||||
"r" => ast::ConversionFlag::Repr,
|
||||
"a" => ast::ConversionFlag::Ascii,
|
||||
_ => Err(LexicalError {
|
||||
error: LexicalErrorType::FStringError(FStringErrorType::InvalidConversionFlag),
|
||||
location,
|
||||
})?
|
||||
};
|
||||
Ok((location, conversion))
|
||||
}
|
||||
};
|
||||
|
||||
Atom<Goal>: ast::ParenthesizedExpr = {
|
||||
<location:@L> <s:(@L string @R)+> =>? Ok(parse_strings(s)?.into()),
|
||||
<location:@L> <strings:StringLiteralOrFString+> <end_location:@R> =>? Ok(concatenate_strings(strings, (location..end_location).into())?.into()),
|
||||
<location:@L> <value:Constant> <end_location:@R> => ast::ExprConstant {
|
||||
value,
|
||||
range: (location..end_location).into(),
|
||||
|
@ -1842,6 +1946,9 @@ extern {
|
|||
Dedent => token::Tok::Dedent,
|
||||
StartModule => token::Tok::StartModule,
|
||||
StartExpression => token::Tok::StartExpression,
|
||||
FStringStart => token::Tok::FStringStart,
|
||||
FStringEnd => token::Tok::FStringEnd,
|
||||
"!" => token::Tok::Exclamation,
|
||||
"?" => token::Tok::Question,
|
||||
"+" => token::Tok::Plus,
|
||||
"-" => token::Tok::Minus,
|
||||
|
@ -1935,6 +2042,10 @@ extern {
|
|||
kind: <StringKind>,
|
||||
triple_quoted: <bool>
|
||||
},
|
||||
fstring_middle => token::Tok::FStringMiddle {
|
||||
value: <String>,
|
||||
is_raw: <bool>
|
||||
},
|
||||
name => token::Tok::Name { name: <String> },
|
||||
ipy_escape_command => token::Tok::IpyEscapeCommand {
|
||||
kind: <IpyEscapeKind>,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
String {
|
||||
value: "",
|
||||
kind: String,
|
||||
triple_quoted: false,
|
||||
},
|
||||
4..6,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
7..9,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
11..13,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
String {
|
||||
value: "",
|
||||
kind: String,
|
||||
triple_quoted: false,
|
||||
},
|
||||
15..17,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
18..22,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
22..25,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
26..30,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
30..33,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
33..33,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "normal ",
|
||||
is_raw: false,
|
||||
},
|
||||
2..9,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "foo",
|
||||
},
|
||||
10..13,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " {another} ",
|
||||
is_raw: false,
|
||||
},
|
||||
14..27,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
27..28,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "bar",
|
||||
},
|
||||
28..31,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
31..32,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " {",
|
||||
is_raw: false,
|
||||
},
|
||||
32..35,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
35..36,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "three",
|
||||
},
|
||||
36..41,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "}",
|
||||
is_raw: false,
|
||||
},
|
||||
42..44,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
44..45,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
45..45,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..4,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\n# not a comment ",
|
||||
is_raw: false,
|
||||
},
|
||||
4..21,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
21..22,
|
||||
),
|
||||
(
|
||||
Comment(
|
||||
"# comment {",
|
||||
),
|
||||
23..34,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
34..35,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
39..40,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
40..41,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " # not a comment\n",
|
||||
is_raw: false,
|
||||
},
|
||||
42..59,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
59..62,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
62..62,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
Exclamation,
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "s",
|
||||
},
|
||||
5..6,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
7..8,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
8..9,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Equal,
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Exclamation,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "r",
|
||||
},
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
15..16,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
17..18,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: ".3f!r",
|
||||
is_raw: false,
|
||||
},
|
||||
18..23,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
23..24,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " {x!r}",
|
||||
is_raw: false,
|
||||
},
|
||||
24..32,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
32..33,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
33..33,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\",
|
||||
is_raw: false,
|
||||
},
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
5..6,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\\"\\",
|
||||
is_raw: false,
|
||||
},
|
||||
6..9,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " \\\"\\\"\\\n end",
|
||||
is_raw: false,
|
||||
},
|
||||
13..24,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
24..25,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
25..25,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\",
|
||||
is_raw: false,
|
||||
},
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "foo",
|
||||
},
|
||||
4..7,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
7..8,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
8..9,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
10..12,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\\\",
|
||||
is_raw: false,
|
||||
},
|
||||
12..14,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "foo",
|
||||
},
|
||||
15..18,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
18..19,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
19..20,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
21..23,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\{foo}",
|
||||
is_raw: false,
|
||||
},
|
||||
23..31,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
31..32,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
33..35,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\\\{foo}",
|
||||
is_raw: false,
|
||||
},
|
||||
35..44,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
44..45,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
45..45,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..3,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\",
|
||||
is_raw: true,
|
||||
},
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
5..6,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\\"\\",
|
||||
is_raw: true,
|
||||
},
|
||||
7..10,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " \\\"\\\"\\\n end",
|
||||
is_raw: true,
|
||||
},
|
||||
14..25,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
25..26,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
26..26,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "first ",
|
||||
is_raw: false,
|
||||
},
|
||||
2..8,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
8..9,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
15..16,
|
||||
),
|
||||
(
|
||||
Star,
|
||||
24..25,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
25..26,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "y",
|
||||
},
|
||||
38..39,
|
||||
),
|
||||
(
|
||||
NonLogicalNewline,
|
||||
39..40,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
40..41,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " second",
|
||||
is_raw: false,
|
||||
},
|
||||
41..48,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
48..49,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
49..49,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..4,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\nhello\n world\n",
|
||||
is_raw: false,
|
||||
},
|
||||
4..21,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
21..24,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
25..29,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\n world\nhello\n",
|
||||
is_raw: false,
|
||||
},
|
||||
29..46,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
46..49,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
50..52,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "some ",
|
||||
is_raw: false,
|
||||
},
|
||||
52..57,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
57..58,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
58..62,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "multiline\nallowed ",
|
||||
is_raw: false,
|
||||
},
|
||||
62..80,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
80..81,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
81..82,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
82..83,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
83..86,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
86..87,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " string",
|
||||
is_raw: false,
|
||||
},
|
||||
87..94,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
94..95,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
95..95,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\N{BULLET} normal \\Nope \\N",
|
||||
is_raw: false,
|
||||
},
|
||||
2..28,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
28..29,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
29..29,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..3,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\N",
|
||||
is_raw: true,
|
||||
},
|
||||
3..5,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
5..6,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "BULLET",
|
||||
},
|
||||
6..12,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " normal",
|
||||
is_raw: true,
|
||||
},
|
||||
13..20,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
20..21,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
21..21,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,163 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "foo ",
|
||||
is_raw: false,
|
||||
},
|
||||
2..6,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
7..9,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "bar ",
|
||||
is_raw: false,
|
||||
},
|
||||
9..13,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
Plus,
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
18..20,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
20..21,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "wow",
|
||||
},
|
||||
21..24,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
24..25,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
25..26,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
26..27,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
27..28,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
28..29,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " baz",
|
||||
is_raw: false,
|
||||
},
|
||||
29..33,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
33..34,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
35..37,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "foo ",
|
||||
is_raw: false,
|
||||
},
|
||||
37..41,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
42..44,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "bar",
|
||||
is_raw: false,
|
||||
},
|
||||
44..47,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
47..48,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
48..49,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " some ",
|
||||
is_raw: false,
|
||||
},
|
||||
49..55,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
55..56,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
56..58,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "another",
|
||||
is_raw: false,
|
||||
},
|
||||
58..65,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
65..66,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
66..67,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
67..68,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
68..68,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
6..8,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "{}",
|
||||
is_raw: false,
|
||||
},
|
||||
8..12,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
14..16,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
17..18,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
18..19,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
19..20,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
21..23,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "{",
|
||||
is_raw: false,
|
||||
},
|
||||
23..25,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
25..26,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
26..27,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "}",
|
||||
is_raw: false,
|
||||
},
|
||||
27..29,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
29..30,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
31..33,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "{{}}",
|
||||
is_raw: false,
|
||||
},
|
||||
33..41,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
43..45,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
45..46,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
46..47,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
47..48,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " {} {",
|
||||
is_raw: false,
|
||||
},
|
||||
48..56,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
56..57,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
57..58,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "} {{}} ",
|
||||
is_raw: false,
|
||||
},
|
||||
58..71,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
71..72,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
72..72,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
4..6,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
8..11,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
13..16,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
18..21,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
21..22,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
23..26,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
26..27,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
28..31,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
31..32,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
33..36,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
36..37,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
38..41,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
43..46,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
46..47,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
47..47,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,201 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "foo",
|
||||
},
|
||||
3..6,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
7..8,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
8..9,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Equal,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Exclamation,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "s",
|
||||
},
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: ".3f",
|
||||
is_raw: false,
|
||||
},
|
||||
15..18,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
18..19,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
19..20,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
20..21,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
21..22,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
22..23,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: ".",
|
||||
is_raw: false,
|
||||
},
|
||||
23..24,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
24..25,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "y",
|
||||
},
|
||||
25..26,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
26..27,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "f",
|
||||
is_raw: false,
|
||||
},
|
||||
27..28,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
28..29,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
29..30,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
30..31,
|
||||
),
|
||||
(
|
||||
String {
|
||||
value: "",
|
||||
kind: String,
|
||||
triple_quoted: false,
|
||||
},
|
||||
31..33,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
33..34,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "*^",
|
||||
is_raw: false,
|
||||
},
|
||||
34..36,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
36..37,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 1,
|
||||
},
|
||||
37..38,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
38..39,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
39..40,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 1,
|
||||
},
|
||||
40..41,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
42..43,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
43..44,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
44..45,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
45..45,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "foo ",
|
||||
is_raw: false,
|
||||
},
|
||||
2..6,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
6..7,
|
||||
),
|
||||
(
|
||||
Exclamation,
|
||||
7..8,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "pwd",
|
||||
},
|
||||
8..11,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " bar",
|
||||
is_raw: false,
|
||||
},
|
||||
12..16,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
17..17,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Lambda,
|
||||
3..9,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
13..14,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
14..15,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
15..16,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
16..17,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
17..18,
|
||||
),
|
||||
(
|
||||
FStringStart,
|
||||
18..20,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
20..21,
|
||||
),
|
||||
(
|
||||
Lpar,
|
||||
21..22,
|
||||
),
|
||||
(
|
||||
Lambda,
|
||||
22..28,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
29..30,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
30..31,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
31..32,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
32..33,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
33..34,
|
||||
),
|
||||
(
|
||||
Rpar,
|
||||
34..35,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
35..36,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
36..37,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
37..37,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
2..3,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
3..4,
|
||||
),
|
||||
(
|
||||
Colon,
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "=10",
|
||||
is_raw: false,
|
||||
},
|
||||
5..8,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
8..9,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
9..10,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
10..11,
|
||||
),
|
||||
(
|
||||
Lpar,
|
||||
11..12,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
12..13,
|
||||
),
|
||||
(
|
||||
ColonEqual,
|
||||
13..15,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 10,
|
||||
},
|
||||
15..17,
|
||||
),
|
||||
(
|
||||
Rpar,
|
||||
17..18,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
18..19,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
19..20,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
20..21,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
21..22,
|
||||
),
|
||||
(
|
||||
Comma,
|
||||
22..23,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
23..24,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "y",
|
||||
},
|
||||
24..25,
|
||||
),
|
||||
(
|
||||
ColonEqual,
|
||||
25..27,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 10,
|
||||
},
|
||||
27..29,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
29..30,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
30..31,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: " ",
|
||||
is_raw: false,
|
||||
},
|
||||
31..32,
|
||||
),
|
||||
(
|
||||
Lbrace,
|
||||
32..33,
|
||||
),
|
||||
(
|
||||
Lsqb,
|
||||
33..34,
|
||||
),
|
||||
(
|
||||
Name {
|
||||
name: "x",
|
||||
},
|
||||
34..35,
|
||||
),
|
||||
(
|
||||
ColonEqual,
|
||||
35..37,
|
||||
),
|
||||
(
|
||||
Int {
|
||||
value: 10,
|
||||
},
|
||||
37..39,
|
||||
),
|
||||
(
|
||||
Rsqb,
|
||||
39..40,
|
||||
),
|
||||
(
|
||||
Rbrace,
|
||||
40..41,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
41..42,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
42..42,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/lexer.rs
|
||||
expression: lex_source(source)
|
||||
---
|
||||
[
|
||||
(
|
||||
FStringStart,
|
||||
0..2,
|
||||
),
|
||||
(
|
||||
FStringMiddle {
|
||||
value: "\\0",
|
||||
is_raw: false,
|
||||
},
|
||||
2..4,
|
||||
),
|
||||
(
|
||||
FStringEnd,
|
||||
4..5,
|
||||
),
|
||||
(
|
||||
Newline,
|
||||
5..5,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,848 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/parser.rs
|
||||
expression: parse_ast
|
||||
---
|
||||
[
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..9,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..9,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 2..8,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 3..7,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " f",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 10..20,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 10..20,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 12..19,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 13..16,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: Str,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 21..28,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 21..28,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 23..27,
|
||||
value: Tuple(
|
||||
ExprTuple {
|
||||
range: 24..26,
|
||||
elts: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 24..25,
|
||||
value: Int(
|
||||
3,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 29..39,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 29..39,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 31..38,
|
||||
value: Compare(
|
||||
ExprCompare {
|
||||
range: 32..36,
|
||||
left: Constant(
|
||||
ExprConstant {
|
||||
range: 32..33,
|
||||
value: Int(
|
||||
3,
|
||||
),
|
||||
},
|
||||
),
|
||||
ops: [
|
||||
NotEq,
|
||||
],
|
||||
comparators: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 35..36,
|
||||
value: Int(
|
||||
4,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 37..37,
|
||||
values: [],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 40..55,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 40..55,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 42..54,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 43..44,
|
||||
value: Int(
|
||||
3,
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 45..53,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 45..50,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 46..49,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "}",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 50..53,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ">10",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 56..71,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 56..71,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 58..70,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 59..60,
|
||||
value: Int(
|
||||
3,
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 61..69,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 61..66,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 62..65,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "{",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 66..69,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ">10",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 72..86,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 72..86,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 74..85,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 77..80,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: " ",
|
||||
trailing: " = ",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 87..107,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 87..107,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 89..106,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 92..95,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: " ",
|
||||
trailing: " = ",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 100..105,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 100..105,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ".3f ",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 108..126,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 108..126,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 110..125,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 113..116,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: " ",
|
||||
trailing: " = ",
|
||||
},
|
||||
),
|
||||
conversion: Str,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 127..143,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 127..143,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 129..142,
|
||||
value: Tuple(
|
||||
ExprTuple {
|
||||
range: 132..136,
|
||||
elts: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 132..133,
|
||||
value: Int(
|
||||
1,
|
||||
),
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 135..136,
|
||||
value: Int(
|
||||
2,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: " ",
|
||||
trailing: " = ",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 144..170,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 144..170,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 146..169,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 147..163,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 149..162,
|
||||
value: Constant(
|
||||
ExprConstant {
|
||||
range: 150..156,
|
||||
value: Float(
|
||||
3.1415,
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 158..161,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 158..161,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ".1f",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 164..168,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 164..168,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "*^20",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 172..206,
|
||||
value: Dict(
|
||||
ExprDict {
|
||||
range: 172..206,
|
||||
keys: [
|
||||
Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 173..201,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 174..186,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foo bar ",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 186..193,
|
||||
value: BinOp(
|
||||
ExprBinOp {
|
||||
range: 187..192,
|
||||
left: Name(
|
||||
ExprName {
|
||||
range: 187..188,
|
||||
id: "x",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
op: Add,
|
||||
right: Name(
|
||||
ExprName {
|
||||
range: 191..192,
|
||||
id: "y",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 193..200,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " baz",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 203..205,
|
||||
value: Int(
|
||||
10,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Match(
|
||||
StmtMatch {
|
||||
range: 207..269,
|
||||
subject: Name(
|
||||
ExprName {
|
||||
range: 213..216,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
cases: [
|
||||
MatchCase {
|
||||
range: 222..269,
|
||||
pattern: MatchValue(
|
||||
PatternMatchValue {
|
||||
range: 227..255,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 227..255,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 228..240,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foo bar ",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 240..247,
|
||||
value: BinOp(
|
||||
ExprBinOp {
|
||||
range: 241..246,
|
||||
left: Name(
|
||||
ExprName {
|
||||
range: 241..242,
|
||||
id: "x",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
op: Add,
|
||||
right: Name(
|
||||
ExprName {
|
||||
range: 245..246,
|
||||
id: "y",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 247..254,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " baz",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
guard: None,
|
||||
body: [
|
||||
Pass(
|
||||
StmtPass {
|
||||
range: 265..269,
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 271..288,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 271..288,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 273..274,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "\\",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 274..279,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 275..278,
|
||||
id: "foo",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 279..280,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "\\",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 280..287,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 281..284,
|
||||
id: "bar",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 285..286,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 285..286,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "\\",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 289..303,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 289..303,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 291..302,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "\\{foo\\}",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/parser.rs
|
||||
expression: parse_ast
|
||||
---
|
||||
[
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..29,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..29,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 2..5,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foo",
|
||||
unicode: true,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 9..14,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 10..13,
|
||||
id: "bar",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 17..28,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "baz some",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 30..59,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 30..59,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 31..34,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foo",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 38..43,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 39..42,
|
||||
id: "bar",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 47..58,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "baz some",
|
||||
unicode: true,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 60..89,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 60..89,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 61..64,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foo",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 68..73,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 69..72,
|
||||
id: "bar",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 76..88,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "baz some",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 90..128,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 90..128,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 92..103,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "foobar ",
|
||||
unicode: true,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 103..108,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 104..107,
|
||||
id: "baz",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: None,
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 108..127,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " reallybarno",
|
||||
unicode: false,
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -3,24 +3,37 @@ source: crates/ruff_python_parser/src/string.rs
|
|||
expression: parse_ast
|
||||
---
|
||||
[
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 2..9,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 3..7,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..10,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..10,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 2..9,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 3..7,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -3,68 +3,81 @@ source: crates/ruff_python_parser/src/string.rs
|
|||
expression: parse_ast
|
||||
---
|
||||
[
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 2..6,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "mix ",
|
||||
unicode: false,
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..38,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..38,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 2..6,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: "mix ",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 6..13,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 7..11,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 13..28,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " with text and ",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 28..37,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 29..35,
|
||||
id: "second",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 6..13,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 7..11,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 13..28,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: " with text and ",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 28..37,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 29..35,
|
||||
id: "second",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: None,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -3,44 +3,57 @@ source: crates/ruff_python_parser/src/string.rs
|
|||
expression: parse_ast
|
||||
---
|
||||
[
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 2..13,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 3..7,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 9..12,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 9..12,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ">10",
|
||||
unicode: false,
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..14,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..14,
|
||||
values: [
|
||||
FormattedValue(
|
||||
ExprFormattedValue {
|
||||
range: 2..13,
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 3..7,
|
||||
id: "user",
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
debug_text: Some(
|
||||
DebugText {
|
||||
leading: "",
|
||||
trailing: "=",
|
||||
},
|
||||
),
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FString(
|
||||
ExprFString {
|
||||
range: 9..12,
|
||||
values: [
|
||||
Constant(
|
||||
ExprConstant {
|
||||
range: 9..12,
|
||||
value: Str(
|
||||
StringConstant {
|
||||
value: ">10",
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/src/string.rs
|
||||
expression: "parse_fstring(\"\").unwrap()"
|
||||
expression: "parse_suite(r#\"f\"\"\"#, \"<test>\").unwrap()"
|
||||
---
|
||||
[]
|
||||
[
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..3,
|
||||
value: FString(
|
||||
ExprFString {
|
||||
range: 0..3,
|
||||
values: [],
|
||||
implicit_concatenated: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue