mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +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"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bitflags 2.4.0",
|
||||||
"insta",
|
"insta",
|
||||||
"is-macro",
|
"is-macro",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
|
|
|
@ -65,7 +65,7 @@ fn benchmark_formatter(criterion: &mut Criterion) {
|
||||||
let comment_ranges = comment_ranges.finish();
|
let comment_ranges = comment_ranges.finish();
|
||||||
|
|
||||||
// Parse the AST.
|
// 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");
|
.expect("Input to be a valid python program");
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
|
|
|
@ -59,3 +59,23 @@ _ = "abc" + "def" + foo
|
||||||
_ = foo + bar + "abc"
|
_ = foo + bar + "abc"
|
||||||
_ = "abc" + foo + bar
|
_ = "abc" + foo + bar
|
||||||
_ = foo + "abc" + 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'
|
'This is a'
|
||||||
'\'string\''
|
'\'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"
|
"This is a"
|
||||||
"\"string\""
|
"\"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 = [ #
|
x = [ #
|
||||||
'some value',
|
'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')],
|
'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
|
#: Okay
|
||||||
a = (1,
|
a = (1,
|
||||||
|
|
|
@ -48,3 +48,9 @@ def add(a: int=0, b: int =0, c: int= 0) -> int:
|
||||||
#: Okay
|
#: Okay
|
||||||
def add(a: int = _default(name='f')):
|
def add(a: int = _default(name='f')):
|
||||||
return a
|
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
|
multiline string with tab in it, different lines
|
||||||
'''
|
'''
|
||||||
" single line string with tab in it"
|
" 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""
|
''f""
|
||||||
(""f""r"")
|
(""f""r"")
|
||||||
|
f"{v:{f"0.2f"}}"
|
||||||
# To be fixed
|
f"\{{x}}"
|
||||||
# Error: f-string: single '}' is not allowed at line 41 column 8
|
|
||||||
# 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
|
# consisting of a single ambiguous character, while the second character is a "word
|
||||||
# boundary" (whitespace) that it itself ambiguous.
|
# boundary" (whitespace) that it itself ambiguous.
|
||||||
x = "Р усский"
|
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);
|
pylint::rules::await_outside_async(checker, expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
Expr::FString(fstring @ ast::ExprFString { values, .. }) => {
|
||||||
if checker.enabled(Rule::FStringMissingPlaceholders) {
|
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) {
|
if checker.enabled(Rule::HardcodedSQLExpression) {
|
||||||
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);
|
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.
|
// Find the quote character used to start the containing f-string.
|
||||||
let expr = self.semantic.current_expression()?;
|
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))?;
|
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
|
||||||
|
|
||||||
// Invert the quote character, if it's a single quote.
|
// 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();
|
let mut state_machine = StateMachine::default();
|
||||||
for &(ref tok, range) in tokens.iter().flatten() {
|
for &(ref tok, range) in tokens.iter().flatten() {
|
||||||
let is_docstring = state_machine.consume(tok);
|
let is_docstring = state_machine.consume(tok);
|
||||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
let context = match tok {
|
||||||
ruff::rules::ambiguous_unicode_character(
|
Tok::String { .. } => {
|
||||||
&mut diagnostics,
|
if is_docstring {
|
||||||
locator,
|
Context::Docstring
|
||||||
range,
|
|
||||||
if tok.is_string() {
|
|
||||||
if is_docstring {
|
|
||||||
Context::Docstring
|
|
||||||
} else {
|
|
||||||
Context::String
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Context::Comment
|
Context::String
|
||||||
},
|
}
|
||||||
settings,
|
}
|
||||||
);
|
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) {
|
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
|
||||||
for (tok, range) in tokens.iter().flatten() {
|
for (tok, range) in tokens.iter().flatten() {
|
||||||
if tok.is_string() {
|
pycodestyle::rules::invalid_escape_sequence(
|
||||||
pycodestyle::rules::invalid_escape_sequence(
|
&mut diagnostics,
|
||||||
&mut diagnostics,
|
locator,
|
||||||
locator,
|
indexer,
|
||||||
*range,
|
tok,
|
||||||
settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
*range,
|
||||||
);
|
settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +100,7 @@ pub(crate) fn check_tokens(
|
||||||
Rule::InvalidCharacterZeroWidthSpace,
|
Rule::InvalidCharacterZeroWidthSpace,
|
||||||
]) {
|
]) {
|
||||||
for (tok, range) in tokens.iter().flatten() {
|
for (tok, range) in tokens.iter().flatten() {
|
||||||
if tok.is_string() {
|
pylint::rules::invalid_string_characters(&mut diagnostics, tok, *range, locator);
|
||||||
pylint::rules::invalid_string_characters(&mut diagnostics, *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(&[
|
if settings.rules.any_enabled(&[
|
||||||
Rule::BadQuotesInlineString,
|
Rule::BadQuotesInlineString,
|
||||||
Rule::BadQuotesMultilineString,
|
Rule::BadQuotesMultilineString,
|
||||||
Rule::BadQuotesDocstring,
|
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(&[
|
if settings.rules.any_enabled(&[
|
||||||
|
@ -136,6 +139,7 @@ pub(crate) fn check_tokens(
|
||||||
tokens,
|
tokens,
|
||||||
&settings.flake8_implicit_str_concat,
|
&settings.flake8_implicit_str_concat,
|
||||||
locator,
|
locator,
|
||||||
|
indexer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! Extract `# noqa`, `# isort: skip`, and `# TODO` directives from tokenized source.
|
//! Extract `# noqa`, `# isort: skip`, and `# TODO` directives from tokenized source.
|
||||||
|
|
||||||
|
use std::iter::Peekable;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitflags::bitflags;
|
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.
|
/// Extract a mapping from logical line to noqa line.
|
||||||
fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer) -> NoqaMapping {
|
fn extract_noqa_line_for(lxr: &[LexResult], locator: &Locator, indexer: &Indexer) -> NoqaMapping {
|
||||||
let mut string_mappings = Vec::new();
|
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();
|
let mut continuation_mappings = Vec::new();
|
||||||
|
|
||||||
// For continuations, we expect `noqa` directives on the last line of the
|
// 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
|
// Merge the mappings in sorted order
|
||||||
let mut mappings =
|
let mut mappings = NoqaMapping::with_capacity(
|
||||||
NoqaMapping::with_capacity(continuation_mappings.len() + string_mappings.len());
|
continuation_mappings.len() + string_mappings.len() + fstring_mappings.len(),
|
||||||
|
);
|
||||||
|
|
||||||
let mut continuation_mappings = continuation_mappings.into_iter().peekable();
|
let string_mappings = SortedMergeIter {
|
||||||
let mut string_mappings = string_mappings.into_iter().peekable();
|
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)) =
|
for mapping in all_mappings {
|
||||||
(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 {
|
|
||||||
mappings.push_mapping(mapping);
|
mappings.push_mapping(mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,6 +479,67 @@ ghi
|
||||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(28))])
|
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 = \
|
let contents = r"x = \
|
||||||
1";
|
1";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -143,6 +143,7 @@ pub fn check_path(
|
||||||
if use_ast || use_imports || use_doc_lines {
|
if use_ast || use_imports || use_doc_lines {
|
||||||
match ruff_python_parser::parse_program_tokens(
|
match ruff_python_parser::parse_program_tokens(
|
||||||
tokens,
|
tokens,
|
||||||
|
source_kind.source_code(),
|
||||||
&path.to_string_lossy(),
|
&path.to_string_lossy(),
|
||||||
source_type.is_ipynb(),
|
source_type.is_ipynb(),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruff_python_parser::lexer::LexResult;
|
use ruff_python_parser::lexer::LexResult;
|
||||||
|
use ruff_python_parser::Tok;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixKind, Violation};
|
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixKind, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||||
|
use ruff_python_index::Indexer;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
|
|
||||||
use crate::rules::flake8_implicit_str_concat::settings::Settings;
|
use crate::rules::flake8_implicit_str_concat::settings::Settings;
|
||||||
|
@ -94,6 +96,7 @@ pub(crate) fn implicit(
|
||||||
tokens: &[LexResult],
|
tokens: &[LexResult],
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
|
indexer: &Indexer,
|
||||||
) {
|
) {
|
||||||
for ((a_tok, a_range), (b_tok, b_range)) in tokens
|
for ((a_tok, a_range), (b_tok, b_range)) in tokens
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -103,24 +106,39 @@ pub(crate) fn implicit(
|
||||||
})
|
})
|
||||||
.tuple_windows()
|
.tuple_windows()
|
||||||
{
|
{
|
||||||
if a_tok.is_string() && b_tok.is_string() {
|
let (a_range, b_range) = match (a_tok, b_tok) {
|
||||||
if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) {
|
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
|
||||||
diagnostics.push(Diagnostic::new(
|
(Tok::String { .. }, Tok::FStringStart) => (
|
||||||
MultiLineImplicitStringConcatenation,
|
*a_range,
|
||||||
TextRange::new(a_range.start(), b_range.end()),
|
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
|
||||||
));
|
),
|
||||||
} else {
|
(Tok::FStringEnd, Tok::String { .. }) => (
|
||||||
let mut diagnostic = Diagnostic::new(
|
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
|
||||||
SingleLineImplicitStringConcatenation,
|
*b_range,
|
||||||
TextRange::new(a_range.start(), b_range.end()),
|
),
|
||||||
);
|
(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) {
|
if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) {
|
||||||
diagnostic.set_fix(fix);
|
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.
|
54 54 | # Single-line explicit concatenation should be ignored.
|
||||||
55 55 | _ = "abc" + "def" + "ghi"
|
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 | _ = (
|
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 | )
|
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.
|
54 54 | # Single-line explicit concatenation should be ignored.
|
||||||
55 55 | _ = "abc" + "def" + "ghi"
|
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 | )
|
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 | )
|
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::assert_messages;
|
||||||
use crate::registry::Rule;
|
use crate::registry::Rule;
|
||||||
|
use crate::settings::types::PythonVersion;
|
||||||
use crate::settings::LinterSettings;
|
use crate::settings::LinterSettings;
|
||||||
use crate::test::test_path;
|
use crate::test::test_path;
|
||||||
|
|
||||||
|
@ -45,6 +46,44 @@ mod tests {
|
||||||
Ok(())
|
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.py"))]
|
||||||
#[test_case(Path::new("singles_escaped.py"))]
|
#[test_case(Path::new("singles_escaped.py"))]
|
||||||
#[test_case(Path::new("singles_implicit.py"))]
|
#[test_case(Path::new("singles_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::lexer::LexResult;
|
||||||
use ruff_python_parser::Tok;
|
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_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
@ -34,22 +34,22 @@ use super::super::settings::Quote;
|
||||||
/// - `flake8-quotes.inline-quotes`
|
/// - `flake8-quotes.inline-quotes`
|
||||||
#[violation]
|
#[violation]
|
||||||
pub struct BadQuotesInlineString {
|
pub struct BadQuotesInlineString {
|
||||||
quote: Quote,
|
preferred_quote: Quote,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlwaysFixableViolation for BadQuotesInlineString {
|
impl AlwaysFixableViolation for BadQuotesInlineString {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let BadQuotesInlineString { quote } = self;
|
let BadQuotesInlineString { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => format!("Single quotes found but double quotes preferred"),
|
Quote::Double => format!("Single quotes found but double quotes preferred"),
|
||||||
Quote::Single => format!("Double quotes found but single quotes preferred"),
|
Quote::Single => format!("Double quotes found but single quotes preferred"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_title(&self) -> String {
|
fn fix_title(&self) -> String {
|
||||||
let BadQuotesInlineString { quote } = self;
|
let BadQuotesInlineString { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => "Replace single quotes with double quotes".to_string(),
|
Quote::Double => "Replace single quotes with double quotes".to_string(),
|
||||||
Quote::Single => "Replace double quotes with single 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`
|
/// - `flake8-quotes.multiline-quotes`
|
||||||
#[violation]
|
#[violation]
|
||||||
pub struct BadQuotesMultilineString {
|
pub struct BadQuotesMultilineString {
|
||||||
quote: Quote,
|
preferred_quote: Quote,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlwaysFixableViolation for BadQuotesMultilineString {
|
impl AlwaysFixableViolation for BadQuotesMultilineString {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let BadQuotesMultilineString { quote } = self;
|
let BadQuotesMultilineString { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => format!("Single quote multiline found but double quotes preferred"),
|
Quote::Double => format!("Single quote multiline found but double quotes preferred"),
|
||||||
Quote::Single => format!("Double quote multiline found but single quotes preferred"),
|
Quote::Single => format!("Double quote multiline found but single quotes preferred"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_title(&self) -> String {
|
fn fix_title(&self) -> String {
|
||||||
let BadQuotesMultilineString { quote } = self;
|
let BadQuotesMultilineString { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => "Replace single multiline quotes with double quotes".to_string(),
|
Quote::Double => "Replace single multiline quotes with double quotes".to_string(),
|
||||||
Quote::Single => "Replace double multiline quotes with single 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`
|
/// - `flake8-quotes.docstring-quotes`
|
||||||
#[violation]
|
#[violation]
|
||||||
pub struct BadQuotesDocstring {
|
pub struct BadQuotesDocstring {
|
||||||
quote: Quote,
|
preferred_quote: Quote,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AlwaysFixableViolation for BadQuotesDocstring {
|
impl AlwaysFixableViolation for BadQuotesDocstring {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let BadQuotesDocstring { quote } = self;
|
let BadQuotesDocstring { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => format!("Single quote docstring found but double quotes preferred"),
|
Quote::Double => format!("Single quote docstring found but double quotes preferred"),
|
||||||
Quote::Single => format!("Double quote docstring found but single quotes preferred"),
|
Quote::Single => format!("Double quote docstring found but single quotes preferred"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_title(&self) -> String {
|
fn fix_title(&self) -> String {
|
||||||
let BadQuotesDocstring { quote } = self;
|
let BadQuotesDocstring { preferred_quote } = self;
|
||||||
match quote {
|
match preferred_quote {
|
||||||
Quote::Double => "Replace single quotes docstring with double quotes".to_string(),
|
Quote::Double => "Replace single quotes docstring with double quotes".to_string(),
|
||||||
Quote::Single => "Replace double quotes docstring with single 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 {
|
const fn good_multiline(quote: Quote) -> &'static str {
|
||||||
match quote {
|
match quote {
|
||||||
Quote::Double => "\"\"\"",
|
Quote::Double => "\"\"\"",
|
||||||
|
@ -219,6 +174,7 @@ const fn good_docstring(quote: Quote) -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Trivia<'a> {
|
struct Trivia<'a> {
|
||||||
last_quote_char: char,
|
last_quote_char: char,
|
||||||
prefix: &'a str,
|
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> {
|
fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) -> Option<Diagnostic> {
|
||||||
let quotes_settings = &settings.flake8_quotes;
|
let quotes_settings = &settings.flake8_quotes;
|
||||||
|
|
||||||
|
@ -270,7 +226,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) ->
|
||||||
|
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
BadQuotesDocstring {
|
BadQuotesDocstring {
|
||||||
quote: quotes_settings.docstring_quotes,
|
preferred_quote: quotes_settings.docstring_quotes,
|
||||||
},
|
},
|
||||||
range,
|
range,
|
||||||
);
|
);
|
||||||
|
@ -292,7 +248,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &LinterSettings) ->
|
||||||
Some(diagnostic)
|
Some(diagnostic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Q001, Q002
|
/// Q000, Q001
|
||||||
fn strings(
|
fn strings(
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
sequence: &[TextRange],
|
sequence: &[TextRange],
|
||||||
|
@ -318,12 +274,12 @@ fn strings(
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let string_contents = &trivia.raw_text[1..trivia.raw_text.len() - 1];
|
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) {
|
for (range, trivia) in sequence.iter().zip(trivia) {
|
||||||
|
@ -346,7 +302,7 @@ fn strings(
|
||||||
|
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
BadQuotesMultilineString {
|
BadQuotesMultilineString {
|
||||||
quote: quotes_settings.multiline_quotes,
|
preferred_quote: quotes_settings.multiline_quotes,
|
||||||
},
|
},
|
||||||
*range,
|
*range,
|
||||||
);
|
);
|
||||||
|
@ -367,107 +323,90 @@ fn strings(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
diagnostics.push(diagnostic);
|
diagnostics.push(diagnostic);
|
||||||
} else {
|
} else if trivia.last_quote_char != quotes_settings.inline_quotes.as_char()
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not using the preferred type, only allow use to avoid escapes.
|
// If we're not using the preferred type, only allow use to avoid escapes.
|
||||||
if !relax_quote {
|
&& !relax_quote
|
||||||
let mut diagnostic = Diagnostic::new(
|
{
|
||||||
BadQuotesInlineString {
|
let mut diagnostic = Diagnostic::new(
|
||||||
quote: quotes_settings.inline_quotes,
|
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,
|
*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
|
diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A builder for the f-string range.
|
||||||
|
///
|
||||||
|
/// For now, this is limited to the outermost f-string and doesn't support
|
||||||
|
/// nested f-strings.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct FStringRangeBuilder {
|
||||||
|
start_location: TextSize,
|
||||||
|
end_location: TextSize,
|
||||||
|
nesting: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FStringRangeBuilder {
|
||||||
|
fn visit_token(&mut self, token: &Tok, range: TextRange) {
|
||||||
|
match token {
|
||||||
|
Tok::FStringStart => {
|
||||||
|
if self.nesting == 0 {
|
||||||
|
self.start_location = range.start();
|
||||||
|
}
|
||||||
|
self.nesting += 1;
|
||||||
|
}
|
||||||
|
Tok::FStringEnd => {
|
||||||
|
self.nesting = self.nesting.saturating_sub(1);
|
||||||
|
if self.nesting == 0 {
|
||||||
|
self.end_location = range.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the lexer is currently inside of a f-string.
|
||||||
|
///
|
||||||
|
/// It'll return `false` once the `FStringEnd` token for the outermost
|
||||||
|
/// f-string is visited.
|
||||||
|
const fn in_fstring(&self) -> bool {
|
||||||
|
self.nesting > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete range of the previously visited f-string.
|
||||||
|
///
|
||||||
|
/// This method should only be called once the lexer is outside of any
|
||||||
|
/// f-string otherwise it might return an invalid range.
|
||||||
|
///
|
||||||
|
/// It doesn't consume the builder because there can be multiple f-strings
|
||||||
|
/// throughout the source code.
|
||||||
|
fn finish(&self) -> TextRange {
|
||||||
|
debug_assert!(!self.in_fstring());
|
||||||
|
TextRange::new(self.start_location, self.end_location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate `flake8-quote` diagnostics from a token stream.
|
/// Generate `flake8-quote` diagnostics from a token stream.
|
||||||
pub(crate) fn from_tokens(
|
pub(crate) fn check_string_quotes(
|
||||||
diagnostics: &mut Vec<Diagnostic>,
|
diagnostics: &mut Vec<Diagnostic>,
|
||||||
lxr: &[LexResult],
|
lxr: &[LexResult],
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
|
@ -477,7 +416,13 @@ pub(crate) fn from_tokens(
|
||||||
// concatenation, and should thus be handled as a single unit.
|
// concatenation, and should thus be handled as a single unit.
|
||||||
let mut sequence = vec![];
|
let mut sequence = vec![];
|
||||||
let mut state_machine = StateMachine::default();
|
let mut state_machine = StateMachine::default();
|
||||||
|
let mut fstring_range_builder = FStringRangeBuilder::default();
|
||||||
for &(ref tok, range) in lxr.iter().flatten() {
|
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);
|
let is_docstring = state_machine.consume(tok);
|
||||||
|
|
||||||
// If this is a docstring, consume the existing sequence, then consume the
|
// If this is a docstring, consume the existing sequence, then consume the
|
||||||
|
@ -491,14 +436,23 @@ pub(crate) fn from_tokens(
|
||||||
diagnostics.push(diagnostic);
|
diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if tok.is_string() {
|
match tok {
|
||||||
// If this is a string, add it to the sequence.
|
Tok::String { .. } => {
|
||||||
sequence.push(range);
|
// If this is a string, add it to the sequence.
|
||||||
} else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) {
|
sequence.push(range);
|
||||||
// Otherwise, consume the sequence.
|
}
|
||||||
if !sequence.is_empty() {
|
Tok::FStringEnd => {
|
||||||
diagnostics.extend(strings(locator, &sequence, settings));
|
// If this is the end of an f-string, add the entire f-string
|
||||||
sequence.clear();
|
// 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\""
|
||||||
9 |+ '"string"'
|
9 |+ '"string"'
|
||||||
10 10 | )
|
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\''
|
||||||
10 |+ "'string'"
|
10 |+ "'string'"
|
||||||
11 11 | )
|
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::BlankLineWithWhitespace, Path::new("W29.py"))]
|
||||||
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.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_1.py"))]
|
||||||
|
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_2.py"))]
|
||||||
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
|
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
|
||||||
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
|
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
|
||||||
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.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_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
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_source_file::Locator;
|
||||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
|
@ -45,29 +46,32 @@ impl AlwaysFixableViolation for InvalidEscapeSequence {
|
||||||
pub(crate) fn invalid_escape_sequence(
|
pub(crate) fn invalid_escape_sequence(
|
||||||
diagnostics: &mut Vec<Diagnostic>,
|
diagnostics: &mut Vec<Diagnostic>,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
range: TextRange,
|
indexer: &Indexer,
|
||||||
|
token: &Tok,
|
||||||
|
token_range: TextRange,
|
||||||
fix: bool,
|
fix: bool,
|
||||||
) {
|
) {
|
||||||
let text = locator.slice(range);
|
let token_source_code = match token {
|
||||||
|
Tok::FStringMiddle { value, is_raw } => {
|
||||||
// Determine whether the string is single- or triple-quoted.
|
if *is_raw {
|
||||||
let Some(leading_quote) = leading_quote(text) else {
|
return;
|
||||||
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 contains_valid_escape_sequence = false;
|
||||||
let mut invalid_escape_sequence = Vec::new();
|
let mut invalid_escape_sequence = Vec::new();
|
||||||
|
|
||||||
let mut prev = None;
|
let mut prev = None;
|
||||||
let bytes = body.as_bytes();
|
let bytes = token_source_code.as_bytes();
|
||||||
for i in memchr_iter(b'\\', bytes) {
|
for i in memchr_iter(b'\\', bytes) {
|
||||||
// If the previous character was also a backslash, skip.
|
// If the previous character was also a backslash, skip.
|
||||||
if prev.is_some_and(|prev| prev == i - 1) {
|
if prev.is_some_and(|prev| prev == i - 1) {
|
||||||
|
@ -77,9 +81,38 @@ pub(crate) fn invalid_escape_sequence(
|
||||||
|
|
||||||
prev = Some(i);
|
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.
|
// If we're at the end of the file, skip.
|
||||||
continue;
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we're at the end of line, skip.
|
// If we're at the end of line, skip.
|
||||||
|
@ -120,7 +153,7 @@ pub(crate) fn invalid_escape_sequence(
|
||||||
continue;
|
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));
|
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
|
||||||
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
|
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
|
||||||
}
|
}
|
||||||
|
@ -135,14 +168,25 @@ pub(crate) fn invalid_escape_sequence(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
} 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.
|
// Turn into raw string.
|
||||||
for diagnostic in &mut invalid_escape_sequence {
|
for diagnostic in &mut invalid_escape_sequence {
|
||||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||||
// `returnr"foo"` is not.
|
// `returnr"foo"` is not.
|
||||||
diagnostic.set_fix(Fix::automatic(Edit::insertion(
|
diagnostic.set_fix(Fix::automatic(Edit::insertion(
|
||||||
pad_start("r".to_string(), range.start(), locator),
|
pad_start("r".to_string(), tok_start, locator),
|
||||||
range.start(),
|
tok_start,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,12 +134,27 @@ pub(crate) fn extraneous_whitespace(
|
||||||
fix_before_punctuation: bool,
|
fix_before_punctuation: bool,
|
||||||
) {
|
) {
|
||||||
let mut prev_token = None;
|
let mut prev_token = None;
|
||||||
|
let mut fstrings = 0u32;
|
||||||
|
|
||||||
for token in line.tokens() {
|
for token in line.tokens() {
|
||||||
let kind = token.kind();
|
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) {
|
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 {
|
match symbol {
|
||||||
BracketOrPunctuation::OpenBracket(symbol) => {
|
BracketOrPunctuation::OpenBracket(symbol) if symbol != '{' || fstrings == 0 => {
|
||||||
let (trailing, trailing_len) = line.trailing_whitespace(token);
|
let (trailing, trailing_len) = line.trailing_whitespace(token);
|
||||||
if !matches!(trailing, Whitespace::None) {
|
if !matches!(trailing, Whitespace::None) {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
@ -153,7 +168,7 @@ pub(crate) fn extraneous_whitespace(
|
||||||
context.push_diagnostic(diagnostic);
|
context.push_diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BracketOrPunctuation::CloseBracket(symbol) => {
|
BracketOrPunctuation::CloseBracket(symbol) if symbol != '}' || fstrings == 0 => {
|
||||||
if !matches!(prev_token, Some(TokenKind::Comma)) {
|
if !matches!(prev_token, Some(TokenKind::Comma)) {
|
||||||
if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) =
|
if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) =
|
||||||
line.leading_whitespace(token)
|
line.leading_whitespace(token)
|
||||||
|
@ -189,6 +204,7 @@ pub(crate) fn extraneous_whitespace(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ impl AlwaysFixableViolation for MissingWhitespace {
|
||||||
/// E231
|
/// E231
|
||||||
pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut LogicalLinesContext) {
|
pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut LogicalLinesContext) {
|
||||||
let mut open_parentheses = 0u32;
|
let mut open_parentheses = 0u32;
|
||||||
|
let mut fstrings = 0u32;
|
||||||
let mut prev_lsqb = TextSize::default();
|
let mut prev_lsqb = TextSize::default();
|
||||||
let mut prev_lbrace = TextSize::default();
|
let mut prev_lbrace = TextSize::default();
|
||||||
let mut iter = line.tokens().iter().peekable();
|
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() {
|
while let Some(token) = iter.next() {
|
||||||
let kind = token.kind();
|
let kind = token.kind();
|
||||||
match kind {
|
match kind {
|
||||||
|
TokenKind::FStringStart => fstrings += 1,
|
||||||
|
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||||
TokenKind::Lsqb => {
|
TokenKind::Lsqb => {
|
||||||
open_parentheses = open_parentheses.saturating_add(1);
|
open_parentheses = open_parentheses.saturating_add(1);
|
||||||
prev_lsqb = token.start();
|
prev_lsqb = token.start();
|
||||||
|
@ -72,6 +75,17 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, fix: bool, context: &mut Lo
|
||||||
TokenKind::Lbrace => {
|
TokenKind::Lbrace => {
|
||||||
prev_lbrace = token.start();
|
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 => {
|
TokenKind::Comma | TokenKind::Semi | TokenKind::Colon => {
|
||||||
let after = line.text_after(token);
|
let after = line.text_after(token);
|
||||||
|
|
|
@ -141,6 +141,7 @@ pub(crate) fn missing_whitespace_around_operator(
|
||||||
prev_token.kind(),
|
prev_token.kind(),
|
||||||
TokenKind::Lpar | TokenKind::Lambda
|
TokenKind::Lpar | TokenKind::Lambda
|
||||||
));
|
));
|
||||||
|
let mut fstrings = u32::from(matches!(prev_token.kind(), TokenKind::FStringStart));
|
||||||
|
|
||||||
while let Some(token) = tokens.next() {
|
while let Some(token) = tokens.next() {
|
||||||
let kind = token.kind();
|
let kind = token.kind();
|
||||||
|
@ -150,13 +151,15 @@ pub(crate) fn missing_whitespace_around_operator(
|
||||||
}
|
}
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
TokenKind::FStringStart => fstrings += 1,
|
||||||
|
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||||
TokenKind::Lpar | TokenKind::Lambda => parens += 1,
|
TokenKind::Lpar | TokenKind::Lambda => parens += 1,
|
||||||
TokenKind::Rpar => parens = parens.saturating_sub(1),
|
TokenKind::Rpar => parens = parens.saturating_sub(1),
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let needs_space = if kind == TokenKind::Equal && parens > 0 {
|
let needs_space = if kind == TokenKind::Equal && (parens > 0 || fstrings > 0) {
|
||||||
// Allow keyword args or defaults: foo(bar=None).
|
// Allow keyword args, defaults: foo(bar=None) and f-strings: f'{foo=}'
|
||||||
NeedsSpace::No
|
NeedsSpace::No
|
||||||
} else if kind == TokenKind::Slash {
|
} else if kind == TokenKind::Slash {
|
||||||
// Tolerate the "/" operator in function definition
|
// Tolerate the "/" operator in function definition
|
||||||
|
|
|
@ -26,7 +26,7 @@ use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineTo
|
||||||
///
|
///
|
||||||
/// Use instead:
|
/// Use instead:
|
||||||
/// ```python
|
/// ```python
|
||||||
/// def add(a = 0) -> int:
|
/// def add(a=0) -> int:
|
||||||
/// return a + 1
|
/// return a + 1
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
|
|
@ -144,4 +144,22 @@ E20.py:81:6: E201 [*] Whitespace after '['
|
||||||
83 83 | #: Okay
|
83 83 | #: Okay
|
||||||
84 84 | x = [ #
|
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})
|
31 31 | spam(ham[1], {eggs: 2})
|
||||||
32 32 |
|
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')],
|
29 |+ 'tag_smalldata': [('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||||
30 30 | }
|
30 30 | }
|
||||||
31 31 |
|
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
|
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_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::{Expr, PySourceType};
|
use ruff_python_ast::{self as ast, Expr, PySourceType};
|
||||||
use ruff_python_parser::{lexer, AsMode, StringKind, Tok};
|
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
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`].
|
/// Return an iterator containing a two-element tuple for each f-string part
|
||||||
fn find_useless_f_strings<'a>(
|
/// in the given [`ExprFString`] expression.
|
||||||
expr: &'a Expr,
|
///
|
||||||
|
/// 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,
|
locator: &'a Locator,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
) -> impl Iterator<Item = (TextRange, TextRange)> + 'a {
|
) -> impl Iterator<Item = (TextRange, TextRange)> + 'a {
|
||||||
let contents = locator.slice(expr);
|
let contents = locator.slice(fstring);
|
||||||
lexer::lex_starts_at(contents, source_type.as_mode(), expr.start())
|
let mut current_f_string_start = fstring.start();
|
||||||
|
lexer::lex_starts_at(contents, source_type.as_mode(), fstring.start())
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|(tok, range)| match tok {
|
.filter_map(move |(tok, range)| match tok {
|
||||||
Tok::String {
|
Tok::FStringStart => {
|
||||||
kind: StringKind::FString | StringKind::RawFString,
|
current_f_string_start = range.start();
|
||||||
..
|
None
|
||||||
} => {
|
}
|
||||||
let first_char = locator.slice(TextRange::at(range.start(), TextSize::from(1)));
|
Tok::FStringEnd => {
|
||||||
|
let first_char =
|
||||||
|
locator.slice(TextRange::at(current_f_string_start, TextSize::from(1)));
|
||||||
// f"..." => f_position = 0
|
// f"..." => f_position = 0
|
||||||
// fr"..." => f_position = 0
|
// fr"..." => f_position = 0
|
||||||
// rf"..." => f_position = 1
|
// rf"..." => f_position = 1
|
||||||
let f_position = u32::from(!(first_char == "f" || first_char == "F"));
|
let f_position = u32::from(!(first_char == "f" || first_char == "F"));
|
||||||
Some((
|
Some((
|
||||||
TextRange::at(
|
TextRange::at(
|
||||||
range.start() + TextSize::from(f_position),
|
current_f_string_start + TextSize::from(f_position),
|
||||||
TextSize::from(1),
|
TextSize::from(1),
|
||||||
),
|
),
|
||||||
range,
|
TextRange::new(current_f_string_start, range.end()),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -79,13 +102,14 @@ fn find_useless_f_strings<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// F541
|
/// F541
|
||||||
pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) {
|
pub(crate) fn f_string_missing_placeholders(fstring: &ast::ExprFString, checker: &mut Checker) {
|
||||||
if !values
|
if !fstring
|
||||||
|
.values
|
||||||
.iter()
|
.iter()
|
||||||
.any(|value| matches!(value, Expr::FormattedValue(_)))
|
.any(|value| matches!(value, Expr::FormattedValue(_)))
|
||||||
{
|
{
|
||||||
for (prefix_range, tok_range) in
|
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);
|
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
|
||||||
if checker.patch(diagnostic.kind.rule()) {
|
if checker.patch(diagnostic.kind.rule()) {
|
||||||
|
|
|
@ -332,7 +332,7 @@ F541.py:40:3: F541 [*] f-string without any placeholders
|
||||||
40 |+"" ""
|
40 |+"" ""
|
||||||
41 41 | ''f""
|
41 41 | ''f""
|
||||||
42 42 | (""f""r"")
|
42 42 | (""f""r"")
|
||||||
43 43 |
|
43 43 | f"{v:{f"0.2f"}}"
|
||||||
|
|
||||||
F541.py:41:3: F541 [*] f-string without any placeholders
|
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""
|
41 | ''f""
|
||||||
| ^^^ F541
|
| ^^^ F541
|
||||||
42 | (""f""r"")
|
42 | (""f""r"")
|
||||||
|
43 | f"{v:{f"0.2f"}}"
|
||||||
|
|
|
|
||||||
= help: Remove extraneous `f` prefix
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
@ -351,8 +352,8 @@ F541.py:41:3: F541 [*] f-string without any placeholders
|
||||||
41 |-''f""
|
41 |-''f""
|
||||||
41 |+''""
|
41 |+''""
|
||||||
42 42 | (""f""r"")
|
42 42 | (""f""r"")
|
||||||
43 43 |
|
43 43 | f"{v:{f"0.2f"}}"
|
||||||
44 44 | # To be fixed
|
44 44 | f"\{{x}}"
|
||||||
|
|
||||||
F541.py:42:4: F541 [*] f-string without any placeholders
|
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""
|
41 | ''f""
|
||||||
42 | (""f""r"")
|
42 | (""f""r"")
|
||||||
| ^^^ F541
|
| ^^^ F541
|
||||||
43 |
|
43 | f"{v:{f"0.2f"}}"
|
||||||
44 | # To be fixed
|
44 | f"\{{x}}"
|
||||||
|
|
|
|
||||||
= help: Remove extraneous `f` prefix
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
@ -371,8 +372,41 @@ F541.py:42:4: F541 [*] f-string without any placeholders
|
||||||
41 41 | ''f""
|
41 41 | ''f""
|
||||||
42 |-(""f""r"")
|
42 |-(""f""r"")
|
||||||
42 |+("" ""r"")
|
42 |+("" ""r"")
|
||||||
43 43 |
|
43 43 | f"{v:{f"0.2f"}}"
|
||||||
44 44 | # To be fixed
|
44 44 | f"\{{x}}"
|
||||||
45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8
|
|
||||||
|
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::Edit;
|
||||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_parser::Tok;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
@ -173,10 +174,15 @@ impl AlwaysFixableViolation for InvalidCharacterZeroWidthSpace {
|
||||||
/// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515
|
/// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515
|
||||||
pub(crate) fn invalid_string_characters(
|
pub(crate) fn invalid_string_characters(
|
||||||
diagnostics: &mut Vec<Diagnostic>,
|
diagnostics: &mut Vec<Diagnostic>,
|
||||||
|
tok: &Tok,
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
locator: &Locator,
|
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}']) {
|
for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) {
|
||||||
let c = match_.chars().next().unwrap();
|
let c = match_.chars().next().unwrap();
|
||||||
|
|
|
@ -7,8 +7,7 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u
|
||||||
14 | #foo = 'hi'
|
14 | #foo = 'hi'
|
||||||
15 | b = ''
|
15 | b = ''
|
||||||
| PLE2510
|
| PLE2510
|
||||||
16 |
|
16 | b = f''
|
||||||
17 | b_ok = '\\b'
|
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
|
@ -18,8 +17,45 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u
|
||||||
14 14 | #foo = 'hi'
|
14 14 | #foo = 'hi'
|
||||||
15 |-b = ''
|
15 |-b = ''
|
||||||
15 |+b = '\b'
|
15 |+b = '\b'
|
||||||
16 16 |
|
16 16 | b = f''
|
||||||
17 17 | b_ok = '\\b'
|
17 17 |
|
||||||
18 18 |
|
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
|
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'
|
22 | cr_ok = f'\\r'
|
||||||
20 |
|
23 |
|
||||||
21 | sub = 'sub '
|
24 | sub = 'sub '
|
||||||
| PLE2512
|
| PLE2512
|
||||||
22 |
|
25 | sub = f'sub '
|
||||||
23 | sub_ok = '\x1a'
|
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
18 18 |
|
21 21 | cr_ok = '\\r'
|
||||||
19 19 | cr_ok = '\\r'
|
22 22 | cr_ok = f'\\r'
|
||||||
20 20 |
|
23 23 |
|
||||||
21 |-sub = 'sub '
|
24 |-sub = 'sub '
|
||||||
21 |+sub = 'sub \x1A'
|
24 |+sub = 'sub \x1A'
|
||||||
22 22 |
|
25 25 | sub = f'sub '
|
||||||
23 23 | sub_ok = '\x1a'
|
26 26 |
|
||||||
24 24 |
|
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
|
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'
|
28 | sub_ok = f'\x1a'
|
||||||
24 |
|
29 |
|
||||||
25 | esc = 'esc esc '
|
30 | esc = 'esc esc '
|
||||||
| PLE2513
|
| PLE2513
|
||||||
26 |
|
31 | esc = f'esc esc '
|
||||||
27 | esc_ok = '\x1b'
|
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
22 22 |
|
27 27 | sub_ok = '\x1a'
|
||||||
23 23 | sub_ok = '\x1a'
|
28 28 | sub_ok = f'\x1a'
|
||||||
24 24 |
|
29 29 |
|
||||||
25 |-esc = 'esc esc '
|
30 |-esc = 'esc esc '
|
||||||
25 |+esc = 'esc esc \x1B'
|
30 |+esc = 'esc esc \x1B'
|
||||||
26 26 |
|
31 31 | esc = f'esc esc '
|
||||||
27 27 | esc_ok = '\x1b'
|
32 32 |
|
||||||
28 28 |
|
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
|
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'
|
42 | nul_ok = f'\0'
|
||||||
33 |
|
43 |
|
||||||
34 | zwsp = 'zerowidth'
|
44 | zwsp = 'zerowidth'
|
||||||
| PLE2515
|
| PLE2515
|
||||||
35 |
|
45 | zwsp = f'zerowidth'
|
||||||
36 | zwsp_ok = '\u200b'
|
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
31 31 |
|
41 41 | nul_ok = '\0'
|
||||||
32 32 | nul_ok = '\0'
|
42 42 | nul_ok = f'\0'
|
||||||
33 33 |
|
43 43 |
|
||||||
34 |-zwsp = 'zerowidth'
|
44 |-zwsp = 'zerowidth'
|
||||||
34 |+zwsp = 'zero\u200bwidth'
|
44 |+zwsp = 'zero\u200bwidth'
|
||||||
35 35 |
|
45 45 | zwsp = f'zerowidth'
|
||||||
36 36 | zwsp_ok = '\u200b'
|
46 46 |
|
||||||
37 37 |
|
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'
|
44 | zwsp = 'zerowidth'
|
||||||
37 |
|
45 | zwsp = f'zerowidth'
|
||||||
38 | zwsp_after_multibyte_character = "ಫ"
|
| 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
|
| PLE2515
|
||||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||||
|
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
35 35 |
|
47 47 | zwsp_ok = '\u200b'
|
||||||
36 36 | zwsp_ok = '\u200b'
|
48 48 | zwsp_ok = f'\u200b'
|
||||||
37 37 |
|
49 49 |
|
||||||
38 |-zwsp_after_multibyte_character = "ಫ"
|
50 |-zwsp_after_multibyte_character = "ಫ"
|
||||||
38 |+zwsp_after_multibyte_character = "ಫ\u200b"
|
50 |+zwsp_after_multibyte_character = "ಫ\u200b"
|
||||||
39 39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
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 = "ಫ"
|
50 | zwsp_after_multibyte_character = "ಫ"
|
||||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
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
|
| PLE2515
|
||||||
|
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
36 36 | zwsp_ok = '\u200b'
|
49 49 |
|
||||||
37 37 |
|
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||||
38 38 | zwsp_after_multibyte_character = "ಫ"
|
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||||
39 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
52 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
39 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
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 = "ಫ"
|
50 | zwsp_after_multibyte_character = "ಫ"
|
||||||
39 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
51 | zwsp_after_multibyte_character = f"ಫ"
|
||||||
|
52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
| PLE2515
|
| PLE2515
|
||||||
|
53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
|
|
|
|
||||||
= help: Replace with escape sequence
|
= help: Replace with escape sequence
|
||||||
|
|
||||||
ℹ Fix
|
ℹ Fix
|
||||||
36 36 | zwsp_ok = '\u200b'
|
49 49 |
|
||||||
37 37 |
|
50 50 | zwsp_after_multibyte_character = "ಫ"
|
||||||
38 38 | zwsp_after_multibyte_character = "ಫ"
|
51 51 | zwsp_after_multibyte_character = f"ಫ"
|
||||||
39 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
52 |-zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ "
|
||||||
39 |+zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ \u200b"
|
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(
|
pub(crate) fn ambiguous_unicode_character(
|
||||||
diagnostics: &mut Vec<Diagnostic>,
|
diagnostics: &mut Vec<Diagnostic>,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
|
|
|
@ -49,6 +49,8 @@ confusables.py:31:6: RUF001 String contains ambiguous `Р` (CYRILLIC CAPITAL LET
|
||||||
30 | # boundary" (whitespace) that it itself ambiguous.
|
30 | # boundary" (whitespace) that it itself ambiguous.
|
||||||
31 | x = "Р усский"
|
31 | x = "Р усский"
|
||||||
| ^ RUF001
|
| ^ 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)?
|
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.
|
30 | # boundary" (whitespace) that it itself ambiguous.
|
||||||
31 | x = "Р усский"
|
31 | x = "Р усский"
|
||||||
| ^ RUF001
|
| ^ 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
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, CacheKey, is_macro::Is)]
|
||||||
|
|
|
@ -2600,6 +2600,14 @@ impl Constant {
|
||||||
_ => false,
|
_ => 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)]
|
#[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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct BytesConstant {
|
pub struct BytesConstant {
|
||||||
/// The bytes value as resolved by the parser (i.e., without quotes, or escape sequences, or
|
/// 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 {
|
impl From<Vec<u8>> for Constant {
|
||||||
fn from(value: Vec<u8>) -> Constant {
|
fn from(value: Vec<u8>) -> Constant {
|
||||||
Self::Bytes(BytesConstant {
|
Self::Bytes(BytesConstant {
|
||||||
|
@ -3207,6 +3234,12 @@ pub struct ParenthesizedExpr {
|
||||||
/// The underlying expression.
|
/// The underlying expression.
|
||||||
pub expr: Expr,
|
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 {
|
impl Ranged for ParenthesizedExpr {
|
||||||
fn range(&self) -> TextRange {
|
fn range(&self) -> TextRange {
|
||||||
self.range
|
self.range
|
||||||
|
|
|
@ -130,7 +130,7 @@ fn function_type_parameters() {
|
||||||
|
|
||||||
fn trace_preorder_visitation(source: &str) -> String {
|
fn trace_preorder_visitation(source: &str) -> String {
|
||||||
let tokens = lex(source, Mode::Module);
|
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();
|
let mut visitor = RecordVisitor::default();
|
||||||
visitor.visit_mod(&parsed);
|
visitor.visit_mod(&parsed);
|
||||||
|
|
|
@ -131,7 +131,7 @@ fn function_type_parameters() {
|
||||||
|
|
||||||
fn trace_visitation(source: &str) -> String {
|
fn trace_visitation(source: &str) -> String {
|
||||||
let tokens = lex(source, Mode::Module);
|
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();
|
let mut visitor = RecordVisitor::default();
|
||||||
walk_module(&mut visitor, &parsed);
|
walk_module(&mut visitor, &parsed);
|
||||||
|
|
|
@ -55,6 +55,9 @@ fn detect_quote(tokens: &[LexResult], locator: &Locator) -> Quote {
|
||||||
triple_quoted: false,
|
triple_quoted: false,
|
||||||
..
|
..
|
||||||
} => Some(*range),
|
} => Some(*range),
|
||||||
|
// No need to check if it's triple-quoted as f-strings cannot be used
|
||||||
|
// as docstrings.
|
||||||
|
Tok::FStringStart => Some(*range),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -275,6 +278,14 @@ class FormFeedIndent:
|
||||||
Quote::Single
|
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 contents = r#"x = "1""#;
|
||||||
let locator = Locator::new(contents);
|
let locator = Locator::new(contents);
|
||||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||||
|
@ -283,6 +294,14 @@ class FormFeedIndent:
|
||||||
Quote::Double
|
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 contents = r#"s = "It's done.""#;
|
||||||
let locator = Locator::new(contents);
|
let locator = Locator::new(contents);
|
||||||
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
|
||||||
|
@ -328,6 +347,41 @@ a = "v"
|
||||||
Stylist::from_tokens(&tokens, &locator).quote(),
|
Stylist::from_tokens(&tokens, &locator).quote(),
|
||||||
Quote::Double
|
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]
|
#[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:?}"))?;
|
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
||||||
|
|
||||||
// Parse the AST.
|
// Parse the AST.
|
||||||
let module =
|
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")
|
||||||
parse_ok_tokens(tokens, Mode::Module, "<filename>").context("Syntax error in input")?;
|
.context("Syntax error in input")?;
|
||||||
|
|
||||||
let options = PyFormatOptions::from_extension(source_type);
|
let options = PyFormatOptions::from_extension(source_type);
|
||||||
|
|
||||||
|
|
|
@ -567,7 +567,7 @@ mod tests {
|
||||||
let source_code = SourceCode::new(source);
|
let source_code = SourceCode::new(source);
|
||||||
let (tokens, comment_ranges) =
|
let (tokens, comment_ranges) =
|
||||||
tokens_and_ranges(source).expect("Expect source to be valid Python");
|
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");
|
.expect("Expect source to be valid Python");
|
||||||
|
|
||||||
CommentsTestCase {
|
CommentsTestCase {
|
||||||
|
|
|
@ -139,7 +139,7 @@ impl<'a> FormatString<'a> {
|
||||||
impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
||||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||||
let locator = f.context().locator();
|
let locator = f.context().locator();
|
||||||
match self.layout {
|
let result = match self.layout {
|
||||||
StringLayout::Default => {
|
StringLayout::Default => {
|
||||||
if self.string.is_implicit_concatenated() {
|
if self.string.is_implicit_concatenated() {
|
||||||
in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f)
|
in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f)
|
||||||
|
@ -162,7 +162,73 @@ impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
|
||||||
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
|
StringLayout::ImplicitConcatenatedStringInBinaryLike => {
|
||||||
FormatStringContinuation::new(self.string).fmt(f)
|
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.
|
// because this is a black preview style.
|
||||||
let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start());
|
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());
|
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
|
||||||
|
|
||||||
for token in lexer {
|
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 {
|
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
|
// ```python
|
||||||
// (
|
// (
|
||||||
// "a"
|
// "a"
|
||||||
|
@ -346,11 +439,7 @@ impl StringPart {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let normalized = normalize_string(
|
let normalized = normalize_string(locator.slice(self.content_range), quotes, self.prefix);
|
||||||
locator.slice(self.content_range),
|
|
||||||
quotes,
|
|
||||||
self.prefix.is_raw_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
NormalizedString {
|
NormalizedString {
|
||||||
prefix: self.prefix,
|
prefix: self.prefix,
|
||||||
|
@ -442,6 +531,10 @@ impl StringPrefix {
|
||||||
pub(super) const fn is_raw_string(self) -> bool {
|
pub(super) const fn is_raw_string(self) -> bool {
|
||||||
self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER)
|
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 {
|
impl Format<PyFormatContext<'_>> for StringPrefix {
|
||||||
|
@ -681,7 +774,7 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
|
||||||
/// with the provided [`StringQuotes`] style.
|
/// with the provided [`StringQuotes`] style.
|
||||||
///
|
///
|
||||||
/// Returns the normalized string and whether it contains new lines.
|
/// 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.
|
// The normalized string if `input` is not yet normalized.
|
||||||
// `output` must remain empty if `input` is already normalized.
|
// `output` must remain empty if `input` is already normalized.
|
||||||
let mut output = String::new();
|
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 preferred_quote = style.as_char();
|
||||||
let opposite_quote = style.invert().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() {
|
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' {
|
if c == '\r' {
|
||||||
output.push_str(&input[last_index..index]);
|
output.push_str(&input[last_index..index]);
|
||||||
|
|
||||||
// Skip over the '\r' character, keep the `\n`
|
// 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();
|
chars.next();
|
||||||
}
|
}
|
||||||
// Replace the `\r` with a `\n`
|
// 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();
|
last_index = index + '\r'.len_utf8();
|
||||||
} else if !quotes.triple && !is_raw {
|
} else if !quotes.triple && !is_raw {
|
||||||
if c == '\\' {
|
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)]
|
#[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
|
// Remove the escape by ending before the backslash and starting again with the quote
|
||||||
chars.next();
|
chars.next();
|
||||||
output.push_str(&input[last_index..index]);
|
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();
|
chars.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if c == preferred_quote {
|
} else if c == preferred_quote && formatted_value_nesting == 0 {
|
||||||
// Escape the quote
|
// Escape the quote
|
||||||
output.push_str(&input[last_index..index]);
|
output.push_str(&input[last_index..index]);
|
||||||
output.push('\\');
|
output.push('\\');
|
||||||
|
|
|
@ -127,7 +127,7 @@ pub fn format_module_source(
|
||||||
options: PyFormatOptions,
|
options: PyFormatOptions,
|
||||||
) -> Result<Printed, FormatModuleError> {
|
) -> Result<Printed, FormatModuleError> {
|
||||||
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
|
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)?;
|
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
|
||||||
Ok(formatted.print()?)
|
Ok(formatted.print()?)
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ def main() -> None:
|
||||||
|
|
||||||
// Parse the AST.
|
// Parse the AST.
|
||||||
let source_path = "code_inline.py";
|
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 options = PyFormatOptions::from_extension(Path::new(source_path));
|
||||||
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();
|
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
|
//! Struct used to index source code, to enable efficient lookup of tokens that
|
||||||
//! are omitted from the AST (e.g., commented lines).
|
//! are omitted from the AST (e.g., commented lines).
|
||||||
|
|
||||||
use crate::CommentRangesBuilder;
|
|
||||||
use ruff_python_ast::Stmt;
|
use ruff_python_ast::Stmt;
|
||||||
use ruff_python_parser::lexer::LexResult;
|
use ruff_python_parser::lexer::LexResult;
|
||||||
use ruff_python_parser::{StringKind, Tok};
|
use ruff_python_parser::Tok;
|
||||||
use ruff_python_trivia::{
|
use ruff_python_trivia::{
|
||||||
has_leading_content, has_trailing_content, is_python_whitespace, CommentRanges,
|
has_leading_content, has_trailing_content, is_python_whitespace, CommentRanges,
|
||||||
};
|
};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
|
||||||
|
use crate::fstring_ranges::{FStringRanges, FStringRangesBuilder};
|
||||||
|
use crate::CommentRangesBuilder;
|
||||||
|
|
||||||
pub struct Indexer {
|
pub struct Indexer {
|
||||||
comment_ranges: CommentRanges,
|
comment_ranges: CommentRanges,
|
||||||
|
|
||||||
/// Stores the start offset of continuation lines.
|
/// Stores the start offset of continuation lines.
|
||||||
continuation_lines: Vec<TextSize>,
|
continuation_lines: Vec<TextSize>,
|
||||||
|
|
||||||
/// The range of all f-string in the source document. The ranges are sorted by their
|
/// The range of all f-string in the source document.
|
||||||
/// [`TextRange::start`] position in increasing order. No two ranges are overlapping.
|
fstring_ranges: FStringRanges,
|
||||||
f_string_ranges: Vec<TextRange>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Indexer {
|
impl Indexer {
|
||||||
|
@ -27,8 +28,8 @@ impl Indexer {
|
||||||
assert!(TextSize::try_from(locator.contents().len()).is_ok());
|
assert!(TextSize::try_from(locator.contents().len()).is_ok());
|
||||||
|
|
||||||
let mut comment_ranges_builder = CommentRangesBuilder::default();
|
let mut comment_ranges_builder = CommentRangesBuilder::default();
|
||||||
|
let mut fstring_ranges_builder = FStringRangesBuilder::default();
|
||||||
let mut continuation_lines = Vec::new();
|
let mut continuation_lines = Vec::new();
|
||||||
let mut f_string_ranges = Vec::new();
|
|
||||||
// Token, end
|
// Token, end
|
||||||
let mut prev_end = TextSize::default();
|
let mut prev_end = TextSize::default();
|
||||||
let mut prev_token: Option<&Tok> = None;
|
let mut prev_token: Option<&Tok> = None;
|
||||||
|
@ -59,18 +60,10 @@ impl Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
comment_ranges_builder.visit_token(tok, *range);
|
comment_ranges_builder.visit_token(tok, *range);
|
||||||
|
fstring_ranges_builder.visit_token(tok, *range);
|
||||||
|
|
||||||
match tok {
|
if matches!(tok, Tok::Newline | Tok::NonLogicalNewline) {
|
||||||
Tok::Newline | Tok::NonLogicalNewline => {
|
line_start = range.end();
|
||||||
line_start = range.end();
|
|
||||||
}
|
|
||||||
Tok::String {
|
|
||||||
kind: StringKind::FString | StringKind::RawFString,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
f_string_ranges.push(*range);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_token = Some(tok);
|
prev_token = Some(tok);
|
||||||
|
@ -79,7 +72,7 @@ impl Indexer {
|
||||||
Self {
|
Self {
|
||||||
comment_ranges: comment_ranges_builder.finish(),
|
comment_ranges: comment_ranges_builder.finish(),
|
||||||
continuation_lines,
|
continuation_lines,
|
||||||
f_string_ranges,
|
fstring_ranges: fstring_ranges_builder.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +81,11 @@ impl Indexer {
|
||||||
&self.comment_ranges
|
&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).
|
/// Returns the line start positions of continuations (backslash).
|
||||||
pub fn continuation_line_starts(&self) -> &[TextSize] {
|
pub fn continuation_line_starts(&self) -> &[TextSize] {
|
||||||
&self.continuation_lines
|
&self.continuation_lines
|
||||||
|
@ -99,22 +97,6 @@ impl Indexer {
|
||||||
self.continuation_lines.binary_search(&line_start).is_ok()
|
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.
|
/// Returns `true` if a statement or expression includes at least one comment.
|
||||||
pub fn has_comments<T>(&self, node: &T, locator: &Locator) -> bool
|
pub fn has_comments<T>(&self, node: &T, locator: &Locator) -> bool
|
||||||
where
|
where
|
||||||
|
@ -250,7 +232,7 @@ mod tests {
|
||||||
use ruff_python_parser::lexer::LexResult;
|
use ruff_python_parser::lexer::LexResult;
|
||||||
use ruff_python_parser::{lexer, Mode};
|
use ruff_python_parser::{lexer, Mode};
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
use ruff_text_size::TextSize;
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
|
|
||||||
use crate::Indexer;
|
use crate::Indexer;
|
||||||
|
|
||||||
|
@ -333,5 +315,203 @@ import os
|
||||||
TextSize::from(116)
|
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 comment_ranges;
|
||||||
|
mod fstring_ranges;
|
||||||
mod indexer;
|
mod indexer;
|
||||||
|
|
||||||
pub use comment_ranges::{tokens_and_ranges, CommentRangesBuilder};
|
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" }
|
ruff_text_size = { path = "../ruff_text_size" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
bitflags = { workspace = true }
|
||||||
is-macro = { workspace = true }
|
is-macro = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
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 ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
use crate::lexer::cursor::{Cursor, EOF_CHAR};
|
use crate::lexer::cursor::{Cursor, EOF_CHAR};
|
||||||
|
use crate::lexer::fstring::{FStringContext, FStringContextFlags, FStrings};
|
||||||
use crate::lexer::indentation::{Indentation, Indentations};
|
use crate::lexer::indentation::{Indentation, Indentations};
|
||||||
use crate::{
|
use crate::{
|
||||||
soft_keywords::SoftKeywordTransformer,
|
soft_keywords::SoftKeywordTransformer,
|
||||||
|
@ -46,6 +47,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod fstring;
|
||||||
mod indentation;
|
mod indentation;
|
||||||
|
|
||||||
/// A lexer for Python source code.
|
/// A lexer for Python source code.
|
||||||
|
@ -62,6 +64,8 @@ pub struct Lexer<'source> {
|
||||||
pending_indentation: Option<Indentation>,
|
pending_indentation: Option<Indentation>,
|
||||||
// Lexer mode.
|
// Lexer mode.
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
// F-string contexts.
|
||||||
|
fstrings: FStrings,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains a Token along with its `range`.
|
/// Contains a Token along with its `range`.
|
||||||
|
@ -154,6 +158,7 @@ impl<'source> Lexer<'source> {
|
||||||
source: input,
|
source: input,
|
||||||
cursor: Cursor::new(input),
|
cursor: Cursor::new(input),
|
||||||
mode,
|
mode,
|
||||||
|
fstrings: FStrings::default(),
|
||||||
};
|
};
|
||||||
// TODO: Handle possible mismatch between BOM and explicit encoding declaration.
|
// TODO: Handle possible mismatch between BOM and explicit encoding declaration.
|
||||||
// spell-checker:ignore feff
|
// 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.
|
/// Lex an identifier. Also used for keywords and string/bytes literals with a prefix.
|
||||||
fn lex_identifier(&mut self, first: char) -> Result<Tok, LexicalError> {
|
fn lex_identifier(&mut self, first: char) -> Result<Tok, LexicalError> {
|
||||||
// Detect potential string like rb'' b'' f'' u'' r''
|
// Detect potential string like rb'' b'' f'' u'' r''
|
||||||
match self.cursor.first() {
|
match (first, self.cursor.first()) {
|
||||||
quote @ ('\'' | '"') => {
|
('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) {
|
if let Ok(string_kind) = StringKind::try_from(first) {
|
||||||
self.cursor.bump();
|
self.cursor.bump();
|
||||||
return self.lex_string(string_kind, quote);
|
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();
|
self.cursor.bump();
|
||||||
|
|
||||||
if let Ok(string_kind) = StringKind::try_from([first, second]) {
|
if let Ok(string_kind) = StringKind::try_from([first, second]) {
|
||||||
let quote = self.cursor.bump().unwrap();
|
let quote = self.cursor.bump().unwrap();
|
||||||
return self.lex_string(string_kind, quote);
|
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.
|
/// Lex a string literal.
|
||||||
fn lex_string(&mut self, kind: StringKind, quote: char) -> Result<Tok, LexicalError> {
|
fn lex_string(&mut self, kind: StringKind, quote: char) -> Result<Tok, LexicalError> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
@ -530,6 +685,19 @@ impl<'source> Lexer<'source> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some('\r' | '\n') if !triple_quoted => {
|
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 {
|
return Err(LexicalError {
|
||||||
error: LexicalErrorType::OtherError(
|
error: LexicalErrorType::OtherError(
|
||||||
"EOL while scanning string literal".to_owned(),
|
"EOL while scanning string literal".to_owned(),
|
||||||
|
@ -549,6 +717,21 @@ impl<'source> Lexer<'source> {
|
||||||
|
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
None => {
|
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 {
|
return Err(LexicalError {
|
||||||
error: if triple_quoted {
|
error: if triple_quoted {
|
||||||
LexicalErrorType::Eof
|
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 is the main entry point. Call this function to retrieve the next token.
|
||||||
// This function is used by the iterator implementation.
|
// This function is used by the iterator implementation.
|
||||||
pub fn next_token(&mut self) -> LexResult {
|
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.
|
// 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) {
|
match self.indentations.current().try_compare(indentation) {
|
||||||
Ok(Ordering::Greater) => {
|
Ok(Ordering::Greater) => {
|
||||||
self.pending_indentation = Some(indentation);
|
self.pending_indentation = Some(indentation);
|
||||||
|
@ -894,10 +1097,7 @@ impl<'source> Lexer<'source> {
|
||||||
if self.cursor.eat_char('=') {
|
if self.cursor.eat_char('=') {
|
||||||
Tok::NotEqual
|
Tok::NotEqual
|
||||||
} else {
|
} else {
|
||||||
return Err(LexicalError {
|
Tok::Exclamation
|
||||||
error: LexicalErrorType::UnrecognizedToken { tok: '!' },
|
|
||||||
location: self.token_start(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'~' => Tok::Tilde,
|
'~' => Tok::Tilde,
|
||||||
|
@ -922,11 +1122,26 @@ impl<'source> Lexer<'source> {
|
||||||
Tok::Lbrace
|
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);
|
self.nesting = self.nesting.saturating_sub(1);
|
||||||
Tok::Rbrace
|
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
|
Tok::ColonEqual
|
||||||
} else {
|
} else {
|
||||||
Tok::Colon
|
Tok::Colon
|
||||||
|
@ -1743,4 +1958,191 @@ def f(arg=%timeit a = b):
|
||||||
.collect();
|
.collect();
|
||||||
assert_debug_snapshot!(tokens);
|
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>
|
pub(super) fn eat_if<F>(&mut self, mut predicate: F) -> Option<char>
|
||||||
where
|
where
|
||||||
F: FnMut(char) -> bool,
|
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)
|
//! return bool(i & 1)
|
||||||
//! "#;
|
//! "#;
|
||||||
//! let tokens = lex(python_source, Mode::Module);
|
//! 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());
|
//! 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.
|
/// Parse a full Python program from its tokens.
|
||||||
pub fn parse_program_tokens(
|
pub fn parse_program_tokens(
|
||||||
lxr: Vec<LexResult>,
|
lxr: Vec<LexResult>,
|
||||||
|
source: &str,
|
||||||
source_path: &str,
|
source_path: &str,
|
||||||
is_jupyter_notebook: bool,
|
is_jupyter_notebook: bool,
|
||||||
) -> anyhow::Result<Suite, ParseError> {
|
) -> anyhow::Result<Suite, ParseError> {
|
||||||
|
@ -154,7 +155,7 @@ pub fn parse_program_tokens(
|
||||||
} else {
|
} else {
|
||||||
Mode::Module
|
Mode::Module
|
||||||
};
|
};
|
||||||
match parse_tokens(lxr, mode, source_path)? {
|
match parse_tokens(lxr, source, mode, source_path)? {
|
||||||
Mod::Module(m) => Ok(m.body),
|
Mod::Module(m) => Ok(m.body),
|
||||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
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> {
|
pub fn parse_program(source: &str, source_path: &str) -> Result<ModModule, ParseError> {
|
||||||
let lexer = lex(source, Mode::Module);
|
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::Module(m) => Ok(m),
|
||||||
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
|
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> {
|
pub fn parse_expression(source: &str, source_path: &str) -> Result<ast::Expr, ParseError> {
|
||||||
let lexer = lex(source, Mode::Expression);
|
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::Expression(expression) => Ok(*expression.body),
|
||||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ pub fn parse_expression_starts_at(
|
||||||
offset: TextSize,
|
offset: TextSize,
|
||||||
) -> Result<ast::Expr, ParseError> {
|
) -> Result<ast::Expr, ParseError> {
|
||||||
let lexer = lex_starts_at(source, Mode::Module, offset);
|
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::Expression(expression) => Ok(*expression.body),
|
||||||
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
Mod::Module(_m) => unreachable!("Mode::Expression doesn't return other variant"),
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ pub fn parse_starts_at(
|
||||||
offset: TextSize,
|
offset: TextSize,
|
||||||
) -> Result<Mod, ParseError> {
|
) -> Result<Mod, ParseError> {
|
||||||
let lxr = lexer::lex_starts_at(source, mode, offset);
|
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`].
|
/// 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};
|
/// 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());
|
/// assert!(expr.is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse_tokens(
|
pub fn parse_tokens(
|
||||||
lxr: impl IntoIterator<Item = LexResult>,
|
lxr: impl IntoIterator<Item = LexResult>,
|
||||||
|
source: &str,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
source_path: &str,
|
source_path: &str,
|
||||||
) -> Result<Mod, ParseError> {
|
) -> Result<Mod, ParseError> {
|
||||||
|
@ -220,6 +222,7 @@ pub fn parse_tokens(
|
||||||
|
|
||||||
parse_filtered_tokens(
|
parse_filtered_tokens(
|
||||||
lxr.filter_ok(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline)),
|
lxr.filter_ok(|(tok, _)| !matches!(tok, Tok::Comment { .. } | Tok::NonLogicalNewline)),
|
||||||
|
source,
|
||||||
mode,
|
mode,
|
||||||
source_path,
|
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.
|
/// Parse tokens into an AST like [`parse_tokens`], but we already know all tokens are valid.
|
||||||
pub fn parse_ok_tokens(
|
pub fn parse_ok_tokens(
|
||||||
lxr: impl IntoIterator<Item = Spanned>,
|
lxr: impl IntoIterator<Item = Spanned>,
|
||||||
|
source: &str,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
source_path: &str,
|
source_path: &str,
|
||||||
) -> Result<Mod, ParseError> {
|
) -> Result<Mod, ParseError> {
|
||||||
|
@ -239,12 +243,13 @@ pub fn parse_ok_tokens(
|
||||||
.chain(lxr)
|
.chain(lxr)
|
||||||
.map(|(t, range)| (range.start(), t, range.end()));
|
.map(|(t, range)| (range.start(), t, range.end()));
|
||||||
python::TopParser::new()
|
python::TopParser::new()
|
||||||
.parse(mode, lexer)
|
.parse(source, mode, lexer)
|
||||||
.map_err(|e| parse_error_from_lalrpop(e, source_path))
|
.map_err(|e| parse_error_from_lalrpop(e, source_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_filtered_tokens(
|
fn parse_filtered_tokens(
|
||||||
lxr: impl IntoIterator<Item = LexResult>,
|
lxr: impl IntoIterator<Item = LexResult>,
|
||||||
|
source: &str,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
source_path: &str,
|
source_path: &str,
|
||||||
) -> Result<Mod, ParseError> {
|
) -> Result<Mod, ParseError> {
|
||||||
|
@ -252,6 +257,7 @@ fn parse_filtered_tokens(
|
||||||
let lexer = iter::once(Ok(marker_token)).chain(lxr);
|
let lexer = iter::once(Ok(marker_token)).chain(lxr);
|
||||||
python::TopParser::new()
|
python::TopParser::new()
|
||||||
.parse(
|
.parse(
|
||||||
|
source,
|
||||||
mode,
|
mode,
|
||||||
lexer.map_ok(|(t, range)| (range.start(), t, range.end())),
|
lexer.map_ok(|(t, range)| (range.start(), t, range.end())),
|
||||||
)
|
)
|
||||||
|
@ -1253,11 +1259,58 @@ a = 1
|
||||||
"#
|
"#
|
||||||
.trim();
|
.trim();
|
||||||
let lxr = lexer::lex_starts_at(source, Mode::Ipython, TextSize::default());
|
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!(
|
assert_eq!(
|
||||||
parse_err.to_string(),
|
parse_err.to_string(),
|
||||||
"IPython escape commands are only allowed in `Mode::Ipython` at byte offset 6"
|
"IPython escape commands are only allowed in `Mode::Ipython` at byte offset 6"
|
||||||
.to_string()
|
.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: file:///usr/share/doc/python/html/reference/compound_stmts.html#function-definitions
|
||||||
// See also: https://greentreesnakes.readthedocs.io/en/latest/nodes.html#keyword
|
// 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 ruff_python_ast::{self as ast, Int, IpyEscapeKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
FStringErrorType,
|
||||||
Mode,
|
Mode,
|
||||||
lexer::{LexicalError, LexicalErrorType},
|
lexer::{LexicalError, LexicalErrorType},
|
||||||
function::{ArgumentList, parse_arguments, validate_pos_params, validate_arguments},
|
function::{ArgumentList, parse_arguments, validate_pos_params, validate_arguments},
|
||||||
context::set_context,
|
context::set_context,
|
||||||
string::parse_strings,
|
string::{StringType, concatenate_strings, parse_fstring_middle, parse_string_literal},
|
||||||
token::{self, StringKind},
|
token::{self, StringKind},
|
||||||
};
|
};
|
||||||
use lalrpop_util::ParseError;
|
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:
|
// This is a hack to reduce the amount of lalrpop tables generated:
|
||||||
// For each public entry point, a full parse table is generated.
|
// For each public entry point, a full parse table is generated.
|
||||||
|
@ -667,8 +668,8 @@ LiteralPattern: ast::Pattern = {
|
||||||
value: Box::new(value.into()),
|
value: Box::new(value.into()),
|
||||||
range: (location..end_location).into()
|
range: (location..end_location).into()
|
||||||
}.into(),
|
}.into(),
|
||||||
<location:@L> <s:(@L string @R)+> <end_location:@R> =>? Ok(ast::PatternMatchValue {
|
<location:@L> <strings:StringLiteralOrFString+> <end_location:@R> =>? Ok(ast::PatternMatchValue {
|
||||||
value: Box::new(parse_strings(s)?),
|
value: Box::new(concatenate_strings(strings, (location..end_location).into())?),
|
||||||
range: (location..end_location).into()
|
range: (location..end_location).into()
|
||||||
}.into()),
|
}.into()),
|
||||||
}
|
}
|
||||||
|
@ -725,7 +726,7 @@ MappingKey: ast::Expr = {
|
||||||
value: false.into(),
|
value: false.into(),
|
||||||
range: (location..end_location).into()
|
range: (location..end_location).into()
|
||||||
}.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) = {
|
MatchMappingEntry: (ast::Expr, ast::Pattern) = {
|
||||||
|
@ -1349,7 +1350,13 @@ NamedExpression: ast::ParenthesizedExpr = {
|
||||||
};
|
};
|
||||||
|
|
||||||
LambdaDef: 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()?;
|
parameters.as_ref().map(validate_arguments).transpose()?;
|
||||||
|
|
||||||
Ok(ast::ExprLambda {
|
Ok(ast::ExprLambda {
|
||||||
|
@ -1572,8 +1579,105 @@ SliceOp: Option<ast::ParenthesizedExpr> = {
|
||||||
<location:@L> ":" <e:Test<"all">?> => e,
|
<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 = {
|
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 {
|
<location:@L> <value:Constant> <end_location:@R> => ast::ExprConstant {
|
||||||
value,
|
value,
|
||||||
range: (location..end_location).into(),
|
range: (location..end_location).into(),
|
||||||
|
@ -1842,6 +1946,9 @@ extern {
|
||||||
Dedent => token::Tok::Dedent,
|
Dedent => token::Tok::Dedent,
|
||||||
StartModule => token::Tok::StartModule,
|
StartModule => token::Tok::StartModule,
|
||||||
StartExpression => token::Tok::StartExpression,
|
StartExpression => token::Tok::StartExpression,
|
||||||
|
FStringStart => token::Tok::FStringStart,
|
||||||
|
FStringEnd => token::Tok::FStringEnd,
|
||||||
|
"!" => token::Tok::Exclamation,
|
||||||
"?" => token::Tok::Question,
|
"?" => token::Tok::Question,
|
||||||
"+" => token::Tok::Plus,
|
"+" => token::Tok::Plus,
|
||||||
"-" => token::Tok::Minus,
|
"-" => token::Tok::Minus,
|
||||||
|
@ -1935,6 +2042,10 @@ extern {
|
||||||
kind: <StringKind>,
|
kind: <StringKind>,
|
||||||
triple_quoted: <bool>
|
triple_quoted: <bool>
|
||||||
},
|
},
|
||||||
|
fstring_middle => token::Tok::FStringMiddle {
|
||||||
|
value: <String>,
|
||||||
|
is_raw: <bool>
|
||||||
|
},
|
||||||
name => token::Tok::Name { name: <String> },
|
name => token::Tok::Name { name: <String> },
|
||||||
ipy_escape_command => token::Tok::IpyEscapeCommand {
|
ipy_escape_command => token::Tok::IpyEscapeCommand {
|
||||||
kind: <IpyEscapeKind>,
|
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
|
expression: parse_ast
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
FormattedValue(
|
Expr(
|
||||||
ExprFormattedValue {
|
StmtExpr {
|
||||||
range: 2..9,
|
range: 0..10,
|
||||||
value: Name(
|
value: FString(
|
||||||
ExprName {
|
ExprFString {
|
||||||
range: 3..7,
|
range: 0..10,
|
||||||
id: "user",
|
values: [
|
||||||
ctx: Load,
|
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
|
expression: parse_ast
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
Constant(
|
Expr(
|
||||||
ExprConstant {
|
StmtExpr {
|
||||||
range: 2..6,
|
range: 0..38,
|
||||||
value: Str(
|
value: FString(
|
||||||
StringConstant {
|
ExprFString {
|
||||||
value: "mix ",
|
range: 0..38,
|
||||||
unicode: false,
|
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,
|
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
|
expression: parse_ast
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
FormattedValue(
|
Expr(
|
||||||
ExprFormattedValue {
|
StmtExpr {
|
||||||
range: 2..13,
|
range: 0..14,
|
||||||
value: Name(
|
value: FString(
|
||||||
ExprName {
|
ExprFString {
|
||||||
range: 3..7,
|
range: 0..14,
|
||||||
id: "user",
|
values: [
|
||||||
ctx: Load,
|
FormattedValue(
|
||||||
},
|
ExprFormattedValue {
|
||||||
),
|
range: 2..13,
|
||||||
debug_text: Some(
|
value: Name(
|
||||||
DebugText {
|
ExprName {
|
||||||
leading: "",
|
range: 3..7,
|
||||||
trailing: "=",
|
id: "user",
|
||||||
},
|
ctx: Load,
|
||||||
),
|
},
|
||||||
conversion: None,
|
),
|
||||||
format_spec: Some(
|
debug_text: Some(
|
||||||
FString(
|
DebugText {
|
||||||
ExprFString {
|
leading: "",
|
||||||
range: 9..12,
|
trailing: "=",
|
||||||
values: [
|
},
|
||||||
Constant(
|
),
|
||||||
ExprConstant {
|
conversion: None,
|
||||||
range: 9..12,
|
format_spec: Some(
|
||||||
value: Str(
|
FString(
|
||||||
StringConstant {
|
ExprFString {
|
||||||
value: ">10",
|
range: 9..12,
|
||||||
unicode: false,
|
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,
|
],
|
||||||
},
|
implicit_concatenated: false,
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_python_parser/src/string.rs
|
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