Skip to content

Commit 03d8338

Browse files
committed
Reject multi-line f-string elements before Python 3.12
1 parent fba6627 commit 03d8338

9 files changed

Lines changed: 332 additions & 117 deletions

File tree

crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/ruff_python_formatter/tests/fixtures.rs
3-
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py
43
---
54
## Input
65
```python
@@ -2436,3 +2435,27 @@ error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python
24362435
179 | f"foo {'"bar"'}"
24372436
|
24382437
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
2438+
2439+
error[invalid-syntax]: Cannot use line breaks in non-triple-quoted f-string replacement fields on Python 3.10 (syntax was added in Python 3.12)
2440+
--> fstring.py:572:8
2441+
|
2442+
570 | ttttteeeeeeeeest,
2443+
571 | ]
2444+
572 | } more {
2445+
| ^
2446+
573 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2447+
574 | }":
2448+
|
2449+
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
2450+
2451+
error[invalid-syntax]: Cannot use line breaks in non-triple-quoted f-string replacement fields on Python 3.10 (syntax was added in Python 3.12)
2452+
--> fstring.py:581:8
2453+
|
2454+
579 | ttttteeeeeeeeest,
2455+
580 | ]
2456+
581 | } more {
2457+
| ^
2458+
582 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2459+
583 | }":
2460+
|
2461+
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.

crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
}'''
77
f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
88
f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
9+
f"{
10+
1
11+
}"
912
f"test {a \
1013
} more" # line continuation
1114
f"""{f"""{x}"""}""" # mark the whole triple quote

crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
f'outer {x:{"# not a comment"} }'
44
f"""{f'''{f'{"# not a comment"}'}'''}"""
55
f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
6+
f"""{
7+
1
8+
}"""
69
f"escape outside of \t {expr}\n"
710
f"test\"abcd"
811
f"{1:\x64}" # escapes are valid in the format spec

crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
}'''
77
f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
88
f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
9+
f"{
10+
1
11+
}"
912
f"test {a \
1013
} more" # line continuation

crates/ruff_python_parser/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ pub enum StarTupleKind {
501501
pub enum FStringKind {
502502
Backslash,
503503
Comment,
504+
LineBreak,
504505
NestedQuote,
505506
}
506507

@@ -732,6 +733,11 @@ pub enum UnsupportedSyntaxErrorKind {
732733
/// bag['bag'] # recursive bags!
733734
/// }'''
734735
///
736+
/// # line breaks in a non-triple-quoted replacement field
737+
/// f"{
738+
/// 1
739+
/// }"
740+
///
735741
/// # arbitrary nesting
736742
/// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}"
737743
/// ```
@@ -991,6 +997,9 @@ impl Display for UnsupportedSyntaxError {
991997
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment) => {
992998
"Cannot use comments in f-strings"
993999
}
1000+
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::LineBreak) => {
1001+
"Cannot use line breaks in non-triple-quoted f-string replacement fields"
1002+
}
9941003
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote) => {
9951004
"Cannot reuse outer quote character in f-strings"
9961005
}

crates/ruff_python_parser/src/parser/expression.rs

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ use crate::string::{
2121
};
2222
use crate::token::TokenValue;
2323
use crate::token_set::TokenSet;
24-
use crate::{
25-
InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError,
26-
UnsupportedSyntaxErrorKind,
27-
};
24+
use crate::{InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind};
2825

2926
use super::{InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind};
3027

@@ -1562,18 +1559,6 @@ impl<'src> Parser<'src> {
15621559
}
15631560
}
15641561

1565-
/// Check `range` for comment tokens and report an `UnsupportedSyntaxError` for each one found.
1566-
fn check_fstring_comments(&mut self, range: TextRange) {
1567-
self.unsupported_syntax_errors
1568-
.extend(self.tokens.in_range(range).iter().filter_map(|token| {
1569-
token.kind().is_comment().then_some(UnsupportedSyntaxError {
1570-
kind: UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment),
1571-
range: token.range(),
1572-
target_version: self.options.target_version,
1573-
})
1574-
}));
1575-
}
1576-
15771562
/// Parses a list of f/t-string elements.
15781563
///
15791564
/// # Panics
@@ -1852,6 +1837,9 @@ impl<'src> Parser<'src> {
18521837
// }'''
18531838
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
18541839
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1840+
// f"{
1841+
// 1
1842+
// }"
18551843
// f"test {a \
18561844
// } more" # line continuation
18571845

@@ -1873,6 +1861,9 @@ impl<'src> Parser<'src> {
18731861
// f'outer {x:{"# not a comment"} }'
18741862
// f"""{f'''{f'{"# not a comment"}'}'''}"""
18751863
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
1864+
// f"""{
1865+
// 1
1866+
// }"""
18761867
// f"escape outside of \t {expr}\n"
18771868
// f"test\"abcd"
18781869
// f"{1:\x64}" # escapes are valid in the format spec
@@ -1887,6 +1878,9 @@ impl<'src> Parser<'src> {
18871878
// }'''
18881879
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
18891880
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1881+
// f"{
1882+
// 1
1883+
// }"
18901884
// f"test {a \
18911885
// } more" # line continuation
18921886
// f"""{f"""{x}"""}""" # mark the whole triple quote
@@ -1918,13 +1912,37 @@ impl<'src> Parser<'src> {
19181912
.map(|format_spec| TextRange::new(range.start(), format_spec.start()))
19191913
.unwrap_or(range);
19201914

1915+
let source = self.source[range].as_bytes();
1916+
let has_line_break =
1917+
!flags.is_triple_quoted() && memchr::memchr2(b'\n', b'\r', source).is_some();
1918+
let backslash_ranges = memchr::memchr_iter(b'\\', source)
1919+
.map(|slash_position| {
1920+
let slash_position = TextSize::try_from(slash_position).unwrap();
1921+
TextRange::at(range.start() + slash_position, '\\'.text_len())
1922+
})
1923+
.collect::<Vec<_>>();
1924+
let comment_ranges = self
1925+
.tokens
1926+
.in_range(range)
1927+
.iter()
1928+
.filter_map(|token| token.kind().is_comment().then_some(token.range()))
1929+
.collect::<Vec<_>>();
1930+
1931+
// Before Python 3.12, replacement fields could only span physical lines when the
1932+
// outer f-string was triple-quoted.
1933+
if has_line_break && backslash_ranges.is_empty() && comment_ranges.is_empty() {
1934+
self.add_unsupported_syntax_error(
1935+
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::LineBreak),
1936+
TextRange::at(range.start(), '{'.text_len()),
1937+
);
1938+
}
1939+
19211940
let quote_bytes = flags.quote_str().as_bytes();
19221941
let quote_len = flags.quote_len();
1923-
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
1924-
let slash_position = TextSize::try_from(slash_position).unwrap();
1942+
for backslash_range in backslash_ranges {
19251943
self.add_unsupported_syntax_error(
19261944
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
1927-
TextRange::at(range.start() + slash_position, '\\'.text_len()),
1945+
backslash_range,
19281946
);
19291947
}
19301948

@@ -1938,7 +1956,12 @@ impl<'src> Parser<'src> {
19381956
);
19391957
}
19401958

1941-
self.check_fstring_comments(range);
1959+
for comment_range in comment_ranges {
1960+
self.add_unsupported_syntax_error(
1961+
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment),
1962+
comment_range,
1963+
);
1964+
}
19421965
}
19431966

19441967
ast::InterpolatedElement {

0 commit comments

Comments
 (0)