mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-22 03:15:44 +00:00
Implement template strings (#17851)
This PR implements template strings (t-strings) in the parser and formatter for Ruff. Minimal changes necessary to compile were made in other parts of the code (e.g. ty, the linter, etc.). These will be covered properly in follow-up PRs.
This commit is contained in:
parent
ad024f9a09
commit
9bbf4987e8
261 changed files with 18023 additions and 1802 deletions
|
@ -6,22 +6,27 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
|
|||
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{
|
||||
self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements,
|
||||
IpyEscapeKind, Number, Operator, OperatorPrecedence, StringFlags, UnaryOp,
|
||||
self as ast, AnyStringFlags, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FString,
|
||||
InterpolatedStringElement, InterpolatedStringElements, IpyEscapeKind, Number, Operator,
|
||||
OperatorPrecedence, StringFlags, TString, UnaryOp,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind};
|
||||
use crate::parser::progress::ParserProgress;
|
||||
use crate::parser::{FunctionKind, Parser, helpers};
|
||||
use crate::string::{StringType, parse_fstring_literal_element, parse_string_literal};
|
||||
use crate::string::{
|
||||
InterpolatedStringKind, StringType, parse_interpolated_string_literal_element,
|
||||
parse_string_literal,
|
||||
};
|
||||
use crate::token::{TokenKind, TokenValue};
|
||||
use crate::token_set::TokenSet;
|
||||
use crate::{
|
||||
FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind,
|
||||
InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError,
|
||||
UnsupportedSyntaxErrorKind,
|
||||
};
|
||||
|
||||
use super::{FStringElementsKind, Parenthesized, RecoveryContextKind};
|
||||
use super::{InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind};
|
||||
|
||||
/// A token set consisting of a newline or end of file.
|
||||
const NEWLINE_EOF_SET: TokenSet = TokenSet::new([TokenKind::Newline, TokenKind::EndOfFile]);
|
||||
|
@ -54,6 +59,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([
|
|||
TokenKind::Not,
|
||||
TokenKind::Yield,
|
||||
TokenKind::FStringStart,
|
||||
TokenKind::TStringStart,
|
||||
TokenKind::IpyEscapeCommand,
|
||||
])
|
||||
.union(LITERAL_SET);
|
||||
|
@ -581,7 +587,9 @@ impl<'src> Parser<'src> {
|
|||
TokenKind::IpyEscapeCommand => {
|
||||
Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression())
|
||||
}
|
||||
TokenKind::String | TokenKind::FStringStart => self.parse_strings(),
|
||||
TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => {
|
||||
self.parse_strings()
|
||||
}
|
||||
TokenKind::Lpar => {
|
||||
return self.parse_parenthesized_expression();
|
||||
}
|
||||
|
@ -1177,12 +1185,15 @@ impl<'src> Parser<'src> {
|
|||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the parser isn't positioned at a `String` or `FStringStart` token.
|
||||
/// If the parser isn't positioned at a `String`, `FStringStart`, or `TStringStart` token.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/grammar.html> (Search "strings:")
|
||||
pub(super) fn parse_strings(&mut self) -> Expr {
|
||||
const STRING_START_SET: TokenSet =
|
||||
TokenSet::new([TokenKind::String, TokenKind::FStringStart]);
|
||||
const STRING_START_SET: TokenSet = TokenSet::new([
|
||||
TokenKind::String,
|
||||
TokenKind::FStringStart,
|
||||
TokenKind::TStringStart,
|
||||
]);
|
||||
|
||||
let start = self.node_start();
|
||||
let mut strings = vec![];
|
||||
|
@ -1194,8 +1205,16 @@ impl<'src> Parser<'src> {
|
|||
|
||||
if self.at(TokenKind::String) {
|
||||
strings.push(self.parse_string_or_byte_literal());
|
||||
} else {
|
||||
strings.push(StringType::FString(self.parse_fstring()));
|
||||
} else if self.at(TokenKind::FStringStart) {
|
||||
strings.push(StringType::FString(
|
||||
self.parse_interpolated_string(InterpolatedStringKind::FString)
|
||||
.into(),
|
||||
));
|
||||
} else if self.at(TokenKind::TStringStart) {
|
||||
strings.push(StringType::TString(
|
||||
self.parse_interpolated_string(InterpolatedStringKind::TString)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1219,6 +1238,10 @@ impl<'src> Parser<'src> {
|
|||
value: ast::FStringValue::single(fstring),
|
||||
range,
|
||||
}),
|
||||
StringType::TString(tstring) => Expr::TString(ast::ExprTString {
|
||||
value: ast::TStringValue::single(tstring),
|
||||
range,
|
||||
}),
|
||||
},
|
||||
_ => self.handle_implicitly_concatenated_strings(strings, range),
|
||||
}
|
||||
|
@ -1236,11 +1259,13 @@ impl<'src> Parser<'src> {
|
|||
) -> Expr {
|
||||
assert!(strings.len() > 1);
|
||||
|
||||
let mut has_tstring = false;
|
||||
let mut has_fstring = false;
|
||||
let mut byte_literal_count = 0;
|
||||
for string in &strings {
|
||||
match string {
|
||||
StringType::FString(_) => has_fstring = true,
|
||||
StringType::TString(_) => has_tstring = true,
|
||||
StringType::Bytes(_) => byte_literal_count += 1,
|
||||
StringType::Str(_) => {}
|
||||
}
|
||||
|
@ -1269,7 +1294,7 @@ impl<'src> Parser<'src> {
|
|||
);
|
||||
}
|
||||
// Only construct a byte expression if all the literals are bytes
|
||||
// otherwise, we'll try either string or f-string. This is to retain
|
||||
// otherwise, we'll try either string, t-string, or f-string. This is to retain
|
||||
// as much information as possible.
|
||||
Ordering::Equal => {
|
||||
let mut values = Vec::with_capacity(strings.len());
|
||||
|
@ -1310,7 +1335,7 @@ impl<'src> Parser<'src> {
|
|||
// )
|
||||
// 2 + 2
|
||||
|
||||
if !has_fstring {
|
||||
if !has_fstring && !has_tstring {
|
||||
let mut values = Vec::with_capacity(strings.len());
|
||||
for string in strings {
|
||||
values.push(match string {
|
||||
|
@ -1324,10 +1349,34 @@ impl<'src> Parser<'src> {
|
|||
});
|
||||
}
|
||||
|
||||
if has_tstring {
|
||||
let mut parts = Vec::with_capacity(strings.len());
|
||||
for string in strings {
|
||||
match string {
|
||||
StringType::TString(tstring) => parts.push(ast::TStringPart::TString(tstring)),
|
||||
StringType::FString(fstring) => {
|
||||
parts.push(ruff_python_ast::TStringPart::FString(fstring));
|
||||
}
|
||||
StringType::Str(string) => parts.push(ast::TStringPart::Literal(string)),
|
||||
StringType::Bytes(bytes) => parts.push(ast::TStringPart::Literal(
|
||||
ast::StringLiteral::invalid(bytes.range()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
return Expr::from(ast::ExprTString {
|
||||
value: ast::TStringValue::concatenated(parts),
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
let mut parts = Vec::with_capacity(strings.len());
|
||||
for string in strings {
|
||||
match string {
|
||||
StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)),
|
||||
StringType::TString(_) => {
|
||||
unreachable!("expected no tstring parts by this point")
|
||||
}
|
||||
StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)),
|
||||
StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal(
|
||||
ast::StringLiteral::invalid(bytes.range()),
|
||||
|
@ -1388,24 +1437,32 @@ impl<'src> Parser<'src> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses a f-string.
|
||||
/// Parses an f/t-string.
|
||||
///
|
||||
/// This does not handle implicitly concatenated strings.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the parser isn't positioned at a `FStringStart` token.
|
||||
/// If the parser isn't positioned at an `FStringStart` or
|
||||
/// `TStringStart` token.
|
||||
///
|
||||
/// See: <https://docs.python.org/3/reference/grammar.html> (Search "fstring:")
|
||||
/// See: <https://docs.python.org/3/reference/grammar.html> (Search "fstring:" or "tstring:")
|
||||
/// See: <https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals>
|
||||
fn parse_fstring(&mut self) -> ast::FString {
|
||||
fn parse_interpolated_string(
|
||||
&mut self,
|
||||
kind: InterpolatedStringKind,
|
||||
) -> InterpolatedStringData {
|
||||
let start = self.node_start();
|
||||
let flags = self.tokens.current_flags().as_any_string_flags();
|
||||
|
||||
self.bump(TokenKind::FStringStart);
|
||||
let elements = self.parse_fstring_elements(flags, FStringElementsKind::Regular);
|
||||
self.bump(kind.start_token());
|
||||
let elements = self.parse_interpolated_string_elements(
|
||||
flags,
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
kind,
|
||||
);
|
||||
|
||||
self.expect(TokenKind::FStringEnd);
|
||||
self.expect(kind.end_token());
|
||||
|
||||
// test_ok pep701_f_string_py312
|
||||
// # parse_options: {"target-version": "3.12"}
|
||||
|
@ -1419,6 +1476,18 @@ impl<'src> Parser<'src> {
|
|||
// f"test {a \
|
||||
// } more" # line continuation
|
||||
|
||||
// test_ok pep750_t_string_py314
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t'Magic wand: { bag['wand'] }' # nested quotes
|
||||
// t"{'\n'.join(a)}" # escape sequence
|
||||
// t'''A complex trick: {
|
||||
// bag['bag'] # comment
|
||||
// }'''
|
||||
// t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
|
||||
// t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
|
||||
// t"test {a \
|
||||
// } more" # line continuation
|
||||
|
||||
// test_ok pep701_f_string_py311
|
||||
// # parse_options: {"target-version": "3.11"}
|
||||
// f"outer {'# not a comment'}"
|
||||
|
@ -1444,10 +1513,12 @@ impl<'src> Parser<'src> {
|
|||
|
||||
let range = self.node_range(start);
|
||||
|
||||
if !self.options.target_version.supports_pep_701() {
|
||||
if !self.options.target_version.supports_pep_701()
|
||||
&& matches!(kind, InterpolatedStringKind::FString)
|
||||
{
|
||||
let quote_bytes = flags.quote_str().as_bytes();
|
||||
let quote_len = flags.quote_len();
|
||||
for expr in elements.expressions() {
|
||||
for expr in elements.interpolations() {
|
||||
for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes())
|
||||
{
|
||||
let slash_position = TextSize::try_from(slash_position).unwrap();
|
||||
|
@ -1471,10 +1542,10 @@ impl<'src> Parser<'src> {
|
|||
self.check_fstring_comments(range);
|
||||
}
|
||||
|
||||
ast::FString {
|
||||
InterpolatedStringData {
|
||||
elements,
|
||||
range,
|
||||
flags: ast::FStringFlags::from(flags),
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1490,80 +1561,87 @@ impl<'src> Parser<'src> {
|
|||
}));
|
||||
}
|
||||
|
||||
/// Parses a list of f-string elements.
|
||||
/// Parses a list of f/t-string elements.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the parser isn't positioned at a `{` or `FStringMiddle` token.
|
||||
fn parse_fstring_elements(
|
||||
/// If the parser isn't positioned at a `{`, `FStringMiddle`,
|
||||
/// or `TStringMiddle` token.
|
||||
fn parse_interpolated_string_elements(
|
||||
&mut self,
|
||||
flags: ast::AnyStringFlags,
|
||||
kind: FStringElementsKind,
|
||||
) -> FStringElements {
|
||||
elements_kind: InterpolatedStringElementsKind,
|
||||
string_kind: InterpolatedStringKind,
|
||||
) -> ast::InterpolatedStringElements {
|
||||
let mut elements = vec![];
|
||||
let middle_token_kind = string_kind.middle_token();
|
||||
|
||||
self.parse_list(RecoveryContextKind::FStringElements(kind), |parser| {
|
||||
let element = match parser.current_token_kind() {
|
||||
TokenKind::Lbrace => {
|
||||
FStringElement::Expression(parser.parse_fstring_expression_element(flags))
|
||||
}
|
||||
TokenKind::FStringMiddle => {
|
||||
let range = parser.current_token_range();
|
||||
let TokenValue::FStringMiddle(value) =
|
||||
parser.bump_value(TokenKind::FStringMiddle)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
FStringElement::Literal(
|
||||
parse_fstring_literal_element(value, flags, range).unwrap_or_else(
|
||||
|lex_error| {
|
||||
// test_err invalid_fstring_literal_element
|
||||
// f'hello \N{INVALID} world'
|
||||
// f"""hello \N{INVALID} world"""
|
||||
let location = lex_error.location();
|
||||
parser.add_error(
|
||||
ParseErrorType::Lexical(lex_error.into_error()),
|
||||
location,
|
||||
);
|
||||
ast::FStringLiteralElement {
|
||||
value: "".into(),
|
||||
range,
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
// `Invalid` tokens are created when there's a lexical error, so
|
||||
// we ignore it here to avoid creating unexpected token errors
|
||||
TokenKind::Unknown => {
|
||||
parser.bump_any();
|
||||
return;
|
||||
}
|
||||
tok => {
|
||||
// This should never happen because the list parsing will only
|
||||
// call this closure for the above token kinds which are the same
|
||||
// as in the FIRST set.
|
||||
unreachable!(
|
||||
"f-string: unexpected token `{tok:?}` at {:?}",
|
||||
parser.current_token_range()
|
||||
);
|
||||
}
|
||||
};
|
||||
elements.push(element);
|
||||
});
|
||||
self.parse_list(
|
||||
RecoveryContextKind::InterpolatedStringElements(elements_kind),
|
||||
|parser| {
|
||||
let element = match parser.current_token_kind() {
|
||||
TokenKind::Lbrace => ast::InterpolatedStringElement::from(
|
||||
parser.parse_interpolated_element(flags, string_kind),
|
||||
),
|
||||
tok if tok == middle_token_kind => {
|
||||
let range = parser.current_token_range();
|
||||
let TokenValue::InterpolatedStringMiddle(value) =
|
||||
parser.bump_value(middle_token_kind)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
InterpolatedStringElement::Literal(
|
||||
parse_interpolated_string_literal_element(value, flags, range)
|
||||
.unwrap_or_else(|lex_error| {
|
||||
// test_err invalid_fstring_literal_element
|
||||
// f'hello \N{INVALID} world'
|
||||
// f"""hello \N{INVALID} world"""
|
||||
let location = lex_error.location();
|
||||
parser.add_error(
|
||||
ParseErrorType::Lexical(lex_error.into_error()),
|
||||
location,
|
||||
);
|
||||
ast::InterpolatedStringLiteralElement {
|
||||
value: "".into(),
|
||||
range,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
// `Invalid` tokens are created when there's a lexical error, so
|
||||
// we ignore it here to avoid creating unexpected token errors
|
||||
TokenKind::Unknown => {
|
||||
parser.bump_any();
|
||||
return;
|
||||
}
|
||||
tok => {
|
||||
// This should never happen because the list parsing will only
|
||||
// call this closure for the above token kinds which are the same
|
||||
// as in the FIRST set.
|
||||
unreachable!(
|
||||
"{}: unexpected token `{tok:?}` at {:?}",
|
||||
string_kind,
|
||||
parser.current_token_range()
|
||||
);
|
||||
}
|
||||
};
|
||||
elements.push(element);
|
||||
},
|
||||
);
|
||||
|
||||
FStringElements::from(elements)
|
||||
ast::InterpolatedStringElements::from(elements)
|
||||
}
|
||||
|
||||
/// Parses a f-string expression element.
|
||||
/// Parses an f/t-string expression element.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the parser isn't positioned at a `{` token.
|
||||
fn parse_fstring_expression_element(
|
||||
fn parse_interpolated_element(
|
||||
&mut self,
|
||||
flags: ast::AnyStringFlags,
|
||||
) -> ast::FStringExpressionElement {
|
||||
string_kind: InterpolatedStringKind,
|
||||
) -> ast::InterpolatedElement {
|
||||
let start = self.node_start();
|
||||
self.bump(TokenKind::Lbrace);
|
||||
|
||||
|
@ -1571,11 +1649,23 @@ impl<'src> Parser<'src> {
|
|||
// f"{}"
|
||||
// f"{ }"
|
||||
|
||||
// test_err t_string_empty_expression
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"{}"
|
||||
// t"{ }"
|
||||
|
||||
// test_err f_string_invalid_starred_expr
|
||||
// # Starred expression inside f-string has a minimum precedence of bitwise or.
|
||||
// f"{*}"
|
||||
// f"{*x and y}"
|
||||
// f"{*yield x}"
|
||||
|
||||
// test_err t_string_invalid_starred_expr
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// # Starred expression inside t-string has a minimum precedence of bitwise or.
|
||||
// t"{*}"
|
||||
// t"{*x and y}"
|
||||
// t"{*yield x}"
|
||||
let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or());
|
||||
|
||||
if !value.is_parenthesized && value.expr.is_lambda_expr() {
|
||||
|
@ -1585,8 +1675,15 @@ impl<'src> Parser<'src> {
|
|||
|
||||
// test_err f_string_lambda_without_parentheses
|
||||
// f"{lambda x: x}"
|
||||
|
||||
// test_err t_string_lambda_without_parentheses
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"{lambda x: x}"
|
||||
self.add_error(
|
||||
ParseErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses),
|
||||
ParseErrorType::from_interpolated_string_error(
|
||||
InterpolatedStringErrorType::LambdaWithoutParentheses,
|
||||
string_kind,
|
||||
),
|
||||
value.range(),
|
||||
);
|
||||
}
|
||||
|
@ -1614,8 +1711,15 @@ impl<'src> Parser<'src> {
|
|||
_ => {
|
||||
// test_err f_string_invalid_conversion_flag_name_tok
|
||||
// f"{x!z}"
|
||||
|
||||
// test_err t_string_invalid_conversion_flag_name_tok
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"{x!z}"
|
||||
self.add_error(
|
||||
ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag),
|
||||
ParseErrorType::from_interpolated_string_error(
|
||||
InterpolatedStringErrorType::InvalidConversionFlag,
|
||||
string_kind,
|
||||
),
|
||||
conversion_flag_range,
|
||||
);
|
||||
ConversionFlag::None
|
||||
|
@ -1625,8 +1729,16 @@ impl<'src> Parser<'src> {
|
|||
// test_err f_string_invalid_conversion_flag_other_tok
|
||||
// f"{x!123}"
|
||||
// f"{x!'a'}"
|
||||
|
||||
// test_err t_string_invalid_conversion_flag_other_tok
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"{x!123}"
|
||||
// t"{x!'a'}"
|
||||
self.add_error(
|
||||
ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag),
|
||||
ParseErrorType::from_interpolated_string_error(
|
||||
InterpolatedStringErrorType::InvalidConversionFlag,
|
||||
string_kind,
|
||||
),
|
||||
conversion_flag_range,
|
||||
);
|
||||
// TODO(dhruvmanila): Avoid dropping this token
|
||||
|
@ -1639,8 +1751,12 @@ impl<'src> Parser<'src> {
|
|||
|
||||
let format_spec = if self.eat(TokenKind::Colon) {
|
||||
let spec_start = self.node_start();
|
||||
let elements = self.parse_fstring_elements(flags, FStringElementsKind::FormatSpec);
|
||||
Some(Box::new(ast::FStringFormatSpec {
|
||||
let elements = self.parse_interpolated_string_elements(
|
||||
flags,
|
||||
InterpolatedStringElementsKind::FormatSpec,
|
||||
string_kind,
|
||||
);
|
||||
Some(Box::new(ast::InterpolatedStringFormatSpec {
|
||||
range: self.node_range(spec_start),
|
||||
elements,
|
||||
}))
|
||||
|
@ -1661,18 +1777,34 @@ impl<'src> Parser<'src> {
|
|||
// f"{"
|
||||
// f"""{"""
|
||||
|
||||
// test_err t_string_unclosed_lbrace
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"{"
|
||||
// t"{foo!r"
|
||||
// t"{foo="
|
||||
// t"{"
|
||||
// t"""{"""
|
||||
|
||||
// The lexer does emit `FStringEnd` for the following test cases:
|
||||
|
||||
// test_err f_string_unclosed_lbrace_in_format_spec
|
||||
// f"hello {x:"
|
||||
// f"hello {x:.3f"
|
||||
|
||||
// test_err t_string_unclosed_lbrace_in_format_spec
|
||||
// # parse_options: {"target-version": "3.14"}
|
||||
// t"hello {x:"
|
||||
// t"hello {x:.3f"
|
||||
self.add_error(
|
||||
ParseErrorType::FStringError(FStringErrorType::UnclosedLbrace),
|
||||
ParseErrorType::from_interpolated_string_error(
|
||||
InterpolatedStringErrorType::UnclosedLbrace,
|
||||
string_kind,
|
||||
),
|
||||
self.current_token_range(),
|
||||
);
|
||||
}
|
||||
|
||||
ast::FStringExpressionElement {
|
||||
ast::InterpolatedElement {
|
||||
expression: Box::new(value.expr),
|
||||
debug_text,
|
||||
conversion,
|
||||
|
@ -2755,3 +2887,30 @@ impl ExpressionContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InterpolatedStringData {
|
||||
elements: InterpolatedStringElements,
|
||||
range: TextRange,
|
||||
flags: AnyStringFlags,
|
||||
}
|
||||
|
||||
impl From<InterpolatedStringData> for FString {
|
||||
fn from(value: InterpolatedStringData) -> Self {
|
||||
Self {
|
||||
elements: value.elements,
|
||||
range: value.range,
|
||||
flags: value.flags.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InterpolatedStringData> for TString {
|
||||
fn from(value: InterpolatedStringData) -> Self {
|
||||
Self {
|
||||
elements: value.elements,
|
||||
range: value.range,
|
||||
flags: value.flags.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ pub(super) fn detect_invalid_pre_py39_decorator_node(
|
|||
Expr::YieldFrom(_) => "`yield from` expression",
|
||||
Expr::Compare(_) => "comparison expression",
|
||||
Expr::FString(_) => "f-string",
|
||||
Expr::TString(_) => "t-string",
|
||||
Expr::Named(_) => "assignment expression",
|
||||
Expr::Subscript(_) => "subscript expression",
|
||||
Expr::IpyEscapeCommand(_) => "IPython escape command",
|
||||
|
|
|
@ -798,7 +798,7 @@ impl WithItemKind {
|
|||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
enum FStringElementsKind {
|
||||
enum InterpolatedStringElementsKind {
|
||||
/// The regular f-string elements.
|
||||
///
|
||||
/// For example, the `"hello "`, `x`, and `" world"` elements in:
|
||||
|
@ -816,14 +816,16 @@ enum FStringElementsKind {
|
|||
FormatSpec,
|
||||
}
|
||||
|
||||
impl FStringElementsKind {
|
||||
const fn list_terminator(self) -> TokenKind {
|
||||
impl InterpolatedStringElementsKind {
|
||||
const fn list_terminators(self) -> TokenSet {
|
||||
match self {
|
||||
FStringElementsKind::Regular => TokenKind::FStringEnd,
|
||||
InterpolatedStringElementsKind::Regular => {
|
||||
TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd])
|
||||
}
|
||||
// test_ok fstring_format_spec_terminator
|
||||
// f"hello {x:} world"
|
||||
// f"hello {x:.3f} world"
|
||||
FStringElementsKind::FormatSpec => TokenKind::Rbrace,
|
||||
InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -931,9 +933,8 @@ enum RecoveryContextKind {
|
|||
/// When parsing a list of items in a `with` statement
|
||||
WithItems(WithItemKind),
|
||||
|
||||
/// When parsing a list of f-string elements which are either literal elements
|
||||
/// or expressions.
|
||||
FStringElements(FStringElementsKind),
|
||||
/// When parsing a list of f-string or t-string elements which are either literal elements, expressions, or interpolations.
|
||||
InterpolatedStringElements(InterpolatedStringElementsKind),
|
||||
}
|
||||
|
||||
impl RecoveryContextKind {
|
||||
|
@ -1117,8 +1118,8 @@ impl RecoveryContextKind {
|
|||
.at(TokenKind::Colon)
|
||||
.then_some(ListTerminatorKind::Regular),
|
||||
},
|
||||
RecoveryContextKind::FStringElements(kind) => {
|
||||
if p.at(kind.list_terminator()) {
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => {
|
||||
if p.at_ts(kind.list_terminators()) {
|
||||
Some(ListTerminatorKind::Regular)
|
||||
} else {
|
||||
// test_err unterminated_fstring_newline_recovery
|
||||
|
@ -1174,10 +1175,10 @@ impl RecoveryContextKind {
|
|||
) || p.at_name_or_soft_keyword()
|
||||
}
|
||||
RecoveryContextKind::WithItems(_) => p.at_expr(),
|
||||
RecoveryContextKind::FStringElements(_) => matches!(
|
||||
RecoveryContextKind::InterpolatedStringElements(_) => matches!(
|
||||
p.current_token_kind(),
|
||||
// Literal element
|
||||
TokenKind::FStringMiddle
|
||||
TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
||||
// Expression element
|
||||
| TokenKind::Lbrace
|
||||
),
|
||||
|
@ -1268,13 +1269,13 @@ impl RecoveryContextKind {
|
|||
"Expected an expression or the end of the with item list".to_string(),
|
||||
),
|
||||
},
|
||||
RecoveryContextKind::FStringElements(kind) => match kind {
|
||||
FStringElementsKind::Regular => ParseErrorType::OtherError(
|
||||
"Expected an f-string element or the end of the f-string".to_string(),
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError(
|
||||
"Expected an f-string or t-string element or the end of the f-string or t-string".to_string(),
|
||||
),
|
||||
InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError(
|
||||
"Expected an f-string or t-string element or a '}'".to_string(),
|
||||
),
|
||||
FStringElementsKind::FormatSpec => {
|
||||
ParseErrorType::OtherError("Expected an f-string element or a '}'".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1313,8 +1314,8 @@ bitflags! {
|
|||
const WITH_ITEMS_PARENTHESIZED = 1 << 25;
|
||||
const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26;
|
||||
const WITH_ITEMS_UNPARENTHESIZED = 1 << 28;
|
||||
const F_STRING_ELEMENTS = 1 << 29;
|
||||
const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30;
|
||||
const FT_STRING_ELEMENTS = 1 << 29;
|
||||
const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1367,10 +1368,10 @@ impl RecoveryContext {
|
|||
}
|
||||
WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED,
|
||||
},
|
||||
RecoveryContextKind::FStringElements(kind) => match kind {
|
||||
FStringElementsKind::Regular => RecoveryContext::F_STRING_ELEMENTS,
|
||||
FStringElementsKind::FormatSpec => {
|
||||
RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC
|
||||
RecoveryContextKind::InterpolatedStringElements(kind) => match kind {
|
||||
InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS,
|
||||
InterpolatedStringElementsKind::FormatSpec => {
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1439,11 +1440,13 @@ impl RecoveryContext {
|
|||
RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => {
|
||||
RecoveryContextKind::WithItems(WithItemKind::Unparenthesized)
|
||||
}
|
||||
RecoveryContext::F_STRING_ELEMENTS => {
|
||||
RecoveryContextKind::FStringElements(FStringElementsKind::Regular)
|
||||
}
|
||||
RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => {
|
||||
RecoveryContextKind::FStringElements(FStringElementsKind::FormatSpec)
|
||||
RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::Regular,
|
||||
),
|
||||
RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => {
|
||||
RecoveryContextKind::InterpolatedStringElements(
|
||||
InterpolatedStringElementsKind::FormatSpec,
|
||||
)
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
|
|
|
@ -390,7 +390,7 @@ impl Parser<'_> {
|
|||
range: self.node_range(start),
|
||||
})
|
||||
}
|
||||
TokenKind::String | TokenKind::FStringStart => {
|
||||
TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => {
|
||||
let str = self.parse_strings();
|
||||
|
||||
Pattern::MatchValue(ast::PatternMatchValue {
|
||||
|
|
|
@ -3012,7 +3012,6 @@ impl<'src> Parser<'src> {
|
|||
// test_ok param_with_annotation
|
||||
// def foo(arg: int): ...
|
||||
// def foo(arg: lambda x: x): ...
|
||||
// def foo(arg: (x := int)): ...
|
||||
|
||||
// test_err param_with_invalid_annotation
|
||||
// def foo(arg: *int): ...
|
||||
|
@ -3703,6 +3702,7 @@ impl<'src> Parser<'src> {
|
|||
| TokenKind::Complex
|
||||
| TokenKind::String
|
||||
| TokenKind::FStringStart
|
||||
| TokenKind::TStringStart
|
||||
| TokenKind::Lbrace
|
||||
| TokenKind::Tilde
|
||||
| TokenKind::Ellipsis
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue