Skip to content

Commit 38c074e

Browse files
authored
Catch syntax errors in nested interpolations before Python 3.12 (#20949)
Summary -- This PR fixes the issue I added in #20867 and noticed in #20930. Cases like this cause an error on any Python version: ```py f"{1:""}" ``` which gave me a false sense of security before. Cases like this are still invalid only before 3.12 and weren't flagged after the changes in #20867: ```py f'{1: abcd "{'aa'}" }' # ^ reused quote f'{1: abcd "{"\n"}" }' # ^ backslash ``` I didn't recognize these as nested interpolations that also need to be checked for invalid expressions, so filtering out the whole format spec wasn't quite right. And `elements.interpolations()` only iterates over the outermost interpolations, not the nested ones. There's basically no code change in this PR, I just moved the existing check from `parse_interpolated_string`, which parses the entire string, to `parse_interpolated_element`. This kind of seems more natural anyway and avoids having to try to recursively visit nested elements after the fact in `parse_interpolated_string`. So viewing the diff with something like ``` git diff --color-moved --ignore-space-change --color-moved-ws=allow-indentation-change main ``` should make this more clear. Test Plan -- New tests
1 parent c2ae9c7 commit 38c074e

File tree

3 files changed

+335
-96
lines changed

3 files changed

+335
-96
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.11"}
2+
# nested interpolations also need to be checked
3+
f'{1: abcd "{'aa'}" }'
4+
f'{1: abcd "{"\n"}" }'

crates/ruff_python_parser/src/parser/expression.rs

Lines changed: 100 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,103 +1539,9 @@ impl<'src> Parser<'src> {
15391539
flags = flags.with_unclosed(true);
15401540
}
15411541

1542-
// test_ok pep701_f_string_py312
1543-
// # parse_options: {"target-version": "3.12"}
1544-
// f'Magic wand: { bag['wand'] }' # nested quotes
1545-
// f"{'\n'.join(a)}" # escape sequence
1546-
// f'''A complex trick: {
1547-
// bag['bag'] # comment
1548-
// }'''
1549-
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
1550-
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1551-
// f"test {a \
1552-
// } more" # line continuation
1553-
1554-
// test_ok pep750_t_string_py314
1555-
// # parse_options: {"target-version": "3.14"}
1556-
// t'Magic wand: { bag['wand'] }' # nested quotes
1557-
// t"{'\n'.join(a)}" # escape sequence
1558-
// t'''A complex trick: {
1559-
// bag['bag'] # comment
1560-
// }'''
1561-
// t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
1562-
// t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
1563-
// t"test {a \
1564-
// } more" # line continuation
1565-
1566-
// test_ok pep701_f_string_py311
1567-
// # parse_options: {"target-version": "3.11"}
1568-
// f"outer {'# not a comment'}"
1569-
// f'outer {x:{"# not a comment"} }'
1570-
// f"""{f'''{f'{"# not a comment"}'}'''}"""
1571-
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
1572-
// f"escape outside of \t {expr}\n"
1573-
// f"test\"abcd"
1574-
// f"{1:\x64}" # escapes are valid in the format spec
1575-
// f"{1:\"d\"}" # this also means that escaped outer quotes are valid
1576-
1577-
// test_err pep701_f_string_py311
1578-
// # parse_options: {"target-version": "3.11"}
1579-
// f'Magic wand: { bag['wand'] }' # nested quotes
1580-
// f"{'\n'.join(a)}" # escape sequence
1581-
// f'''A complex trick: {
1582-
// bag['bag'] # comment
1583-
// }'''
1584-
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
1585-
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1586-
// f"test {a \
1587-
// } more" # line continuation
1588-
// f"""{f"""{x}"""}""" # mark the whole triple quote
1589-
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
1590-
1591-
// test_err nested_quote_in_format_spec_py312
1592-
// # parse_options: {"target-version": "3.12"}
1593-
// f"{1:""}" # this is a ParseError on all versions
1594-
1595-
// test_ok non_nested_quote_in_format_spec_py311
1596-
// # parse_options: {"target-version": "3.11"}
1597-
// f"{1:''}" # but this is okay on all versions
1598-
let range = self.node_range(start);
1599-
1600-
if !self.options.target_version.supports_pep_701()
1601-
&& matches!(kind, InterpolatedStringKind::FString)
1602-
{
1603-
let quote_bytes = flags.quote_str().as_bytes();
1604-
let quote_len = flags.quote_len();
1605-
for expr in elements.interpolations() {
1606-
// We need to check the whole expression range, including any leading or trailing
1607-
// debug text, but exclude the format spec, where escapes and escaped, reused quotes
1608-
// are allowed.
1609-
let range = expr
1610-
.format_spec
1611-
.as_ref()
1612-
.map(|format_spec| TextRange::new(expr.start(), format_spec.start()))
1613-
.unwrap_or(expr.range);
1614-
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
1615-
let slash_position = TextSize::try_from(slash_position).unwrap();
1616-
self.add_unsupported_syntax_error(
1617-
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
1618-
TextRange::at(range.start() + slash_position, '\\'.text_len()),
1619-
);
1620-
}
1621-
1622-
if let Some(quote_position) =
1623-
memchr::memmem::find(self.source[range].as_bytes(), quote_bytes)
1624-
{
1625-
let quote_position = TextSize::try_from(quote_position).unwrap();
1626-
self.add_unsupported_syntax_error(
1627-
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
1628-
TextRange::at(range.start() + quote_position, quote_len),
1629-
);
1630-
}
1631-
}
1632-
1633-
self.check_fstring_comments(range);
1634-
}
1635-
16361542
InterpolatedStringData {
16371543
elements,
1638-
range,
1544+
range: self.node_range(start),
16391545
flags,
16401546
}
16411547
}
@@ -1921,12 +1827,110 @@ impl<'src> Parser<'src> {
19211827
);
19221828
}
19231829

1830+
// test_ok pep701_f_string_py312
1831+
// # parse_options: {"target-version": "3.12"}
1832+
// f'Magic wand: { bag['wand'] }' # nested quotes
1833+
// f"{'\n'.join(a)}" # escape sequence
1834+
// f'''A complex trick: {
1835+
// bag['bag'] # comment
1836+
// }'''
1837+
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
1838+
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1839+
// f"test {a \
1840+
// } more" # line continuation
1841+
1842+
// test_ok pep750_t_string_py314
1843+
// # parse_options: {"target-version": "3.14"}
1844+
// t'Magic wand: { bag['wand'] }' # nested quotes
1845+
// t"{'\n'.join(a)}" # escape sequence
1846+
// t'''A complex trick: {
1847+
// bag['bag'] # comment
1848+
// }'''
1849+
// t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
1850+
// t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
1851+
// t"test {a \
1852+
// } more" # line continuation
1853+
1854+
// test_ok pep701_f_string_py311
1855+
// # parse_options: {"target-version": "3.11"}
1856+
// f"outer {'# not a comment'}"
1857+
// f'outer {x:{"# not a comment"} }'
1858+
// f"""{f'''{f'{"# not a comment"}'}'''}"""
1859+
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
1860+
// f"escape outside of \t {expr}\n"
1861+
// f"test\"abcd"
1862+
// f"{1:\x64}" # escapes are valid in the format spec
1863+
// f"{1:\"d\"}" # this also means that escaped outer quotes are valid
1864+
1865+
// test_err pep701_f_string_py311
1866+
// # parse_options: {"target-version": "3.11"}
1867+
// f'Magic wand: { bag['wand'] }' # nested quotes
1868+
// f"{'\n'.join(a)}" # escape sequence
1869+
// f'''A complex trick: {
1870+
// bag['bag'] # comment
1871+
// }'''
1872+
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
1873+
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
1874+
// f"test {a \
1875+
// } more" # line continuation
1876+
// f"""{f"""{x}"""}""" # mark the whole triple quote
1877+
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
1878+
1879+
// test_err pep701_nested_interpolation_py311
1880+
// # parse_options: {"target-version": "3.11"}
1881+
// # nested interpolations also need to be checked
1882+
// f'{1: abcd "{'aa'}" }'
1883+
// f'{1: abcd "{"\n"}" }'
1884+
1885+
// test_err nested_quote_in_format_spec_py312
1886+
// # parse_options: {"target-version": "3.12"}
1887+
// f"{1:""}" # this is a ParseError on all versions
1888+
1889+
// test_ok non_nested_quote_in_format_spec_py311
1890+
// # parse_options: {"target-version": "3.11"}
1891+
// f"{1:''}" # but this is okay on all versions
1892+
let range = self.node_range(start);
1893+
1894+
if !self.options.target_version.supports_pep_701()
1895+
&& matches!(string_kind, InterpolatedStringKind::FString)
1896+
{
1897+
// We need to check the whole expression range, including any leading or trailing
1898+
// debug text, but exclude the format spec, where escapes and escaped, reused quotes
1899+
// are allowed.
1900+
let range = format_spec
1901+
.as_ref()
1902+
.map(|format_spec| TextRange::new(range.start(), format_spec.start()))
1903+
.unwrap_or(range);
1904+
1905+
let quote_bytes = flags.quote_str().as_bytes();
1906+
let quote_len = flags.quote_len();
1907+
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
1908+
let slash_position = TextSize::try_from(slash_position).unwrap();
1909+
self.add_unsupported_syntax_error(
1910+
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
1911+
TextRange::at(range.start() + slash_position, '\\'.text_len()),
1912+
);
1913+
}
1914+
1915+
if let Some(quote_position) =
1916+
memchr::memmem::find(self.source[range].as_bytes(), quote_bytes)
1917+
{
1918+
let quote_position = TextSize::try_from(quote_position).unwrap();
1919+
self.add_unsupported_syntax_error(
1920+
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
1921+
TextRange::at(range.start() + quote_position, quote_len),
1922+
);
1923+
}
1924+
1925+
self.check_fstring_comments(range);
1926+
}
1927+
19241928
ast::InterpolatedElement {
19251929
expression: Box::new(value.expr),
19261930
debug_text,
19271931
conversion,
19281932
format_spec,
1929-
range: self.node_range(start),
1933+
range,
19301934
node_index: AtomicNodeIndex::NONE,
19311935
}
19321936
}

0 commit comments

Comments
 (0)