diff --git a/crates/ruff/src/checkers/ast/analyze/expression.rs b/crates/ruff/src/checkers/ast/analyze/expression.rs index 20352a1067f51..a9b08ef20fe0a 100644 --- a/crates/ruff/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff/src/checkers/ast/analyze/expression.rs @@ -931,15 +931,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { pylint::rules::await_outside_async(checker, expr); } } - Expr::FString(ast::ExprFString { values, .. }) => { + Expr::FString(ast::ExprFString { parts, .. }) => { if checker.enabled(Rule::FStringMissingPlaceholders) { - pyflakes::rules::f_string_missing_placeholders(expr, values, checker); + pyflakes::rules::f_string_missing_placeholders(expr, parts, checker); } if checker.enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(checker, expr); } if checker.enabled(Rule::ExplicitFStringTypeConversion) { - ruff::rules::explicit_f_string_type_conversion(checker, expr, values); + ruff::rules::explicit_f_string_type_conversion(checker, expr, parts); } } Expr::BinOp(ast::ExprBinOp { diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d19d212b1979e..5a980efea3716 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1283,17 +1283,6 @@ where self.semantic.flags = flags_snapshot; } - fn visit_format_spec(&mut self, format_spec: &'b Expr) { - match format_spec { - Expr::FString(ast::ExprFString { values, .. }) => { - for value in values { - self.visit_expr(value); - } - } - _ => unreachable!("Unexpected expression for format_spec"), - } - } - fn visit_parameters(&mut self, parameters: &'b Parameters) { // Step 1: Binding. // Bind, but intentionally avoid walking default expressions, as we handle them diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index 8887808b5c09c..49858ccd6d642 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -1004,7 +1004,7 @@ pub(crate) fn fix_unnecessary_map( // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. if matches!(object_type, ObjectType::Set | ObjectType::Dict) { - if parent.is_some_and(Expr::is_formatted_value_expr) { + if parent.is_some_and(Expr::is_f_string_expr) { content = format!(" {content} "); } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs index 43a183267fa78..015a4971a733a 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs @@ -63,13 +63,22 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool { .. }) => string.is_empty(), Expr::Constant(constant) if constant.value.is_none() => true, - Expr::FString(ast::ExprFString { values, .. }) => { - values.iter().all(is_empty_or_null_string) + Expr::FString(ast::ExprFString { parts, .. }) => { + parts.iter().all(is_empty_or_null_string_part) } _ => false, } } +fn is_empty_or_null_string_part(part: &ast::FStringPart) -> bool { + match part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => value.is_empty(), + ast::FStringPart::FormattedValue(ast::FormattedValue { expression, .. }) => { + is_empty_or_null_string(expression) + } + } +} + pub(super) fn split_names(names: &str) -> Vec<&str> { // Match the following pytest code: // [x.strip() for x in argnames.split(",") if x.strip()] diff --git a/crates/ruff/src/rules/flynt/helpers.rs b/crates/ruff/src/rules/flynt/helpers.rs index 35183bb020305..05aad10f3b10b 100644 --- a/crates/ruff/src/rules/flynt/helpers.rs +++ b/crates/ruff/src/rules/flynt/helpers.rs @@ -2,25 +2,22 @@ use ruff_python_ast::{self as ast, Arguments, Constant, ConversionFlag, Expr}; use ruff_text_size::TextRange; /// Wrap an expression in a `FormattedValue` with no special formatting. -fn to_formatted_value_expr(inner: &Expr) -> Expr { - let node = ast::ExprFormattedValue { - value: Box::new(inner.clone()), +fn to_formatted_value_expr(inner: &Expr) -> ast::FStringPart { + ast::FStringPart::FormattedValue(ast::FormattedValue { + expression: Box::new(inner.clone()), debug_text: None, conversion: ConversionFlag::None, - format_spec: None, + format_spec: vec![], range: TextRange::default(), - }; - node.into() + }) } /// Convert a string to a constant string expression. -pub(super) fn to_constant_string(s: &str) -> Expr { - let node = ast::ExprConstant { - value: s.to_owned().into(), - kind: None, +pub(super) fn to_constant_string(s: &str) -> ast::FStringPart { + ast::FStringPart::Literal(ast::PartialString { + value: s.to_owned(), range: TextRange::default(), - }; - node.into() + }) } /// Figure out if `expr` represents a "simple" call @@ -52,15 +49,17 @@ fn is_simple_callee(func: &Expr) -> bool { } /// Convert an expression to a f-string element (if it looks like a good idea). -pub(super) fn to_f_string_element(expr: &Expr) -> Option { +pub(super) fn to_fstring_part(expr: &Expr) -> Option { match expr { // These are directly handled by `unparse_f_string_element`: Expr::Constant(ast::ExprConstant { - value: Constant::Str(_), + value: Constant::Str(value), + range, .. - }) - | Expr::FString(_) - | Expr::FormattedValue(_) => Some(expr.clone()), + }) => Some(ast::FStringPart::Literal(ast::PartialString { + value: value.to_string(), + range: *range, + })), // These should be pretty safe to wrap in a formatted value. Expr::Constant(ast::ExprConstant { value: diff --git a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs index 980680bb85b2b..2606f3a2d7307 100644 --- a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs @@ -91,7 +91,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { return Some(node.into()); } - let mut fstring_elems = Vec::with_capacity(joinees.len() * 2); + let mut fstring_parts = Vec::with_capacity(joinees.len() * 2); let mut first = true; for expr in joinees { @@ -101,13 +101,13 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { return None; } if !std::mem::take(&mut first) { - fstring_elems.push(helpers::to_constant_string(joiner)); + fstring_parts.push(helpers::to_constant_string(joiner)); } - fstring_elems.push(helpers::to_f_string_element(expr)?); + fstring_parts.push(helpers::to_fstring_part(expr)?); } let node = ast::ExprFString { - values: fstring_elems, + parts: fstring_parts, implicit_concatenated: false, range: TextRange::default(), }; diff --git a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 5606cb74c32e5..4773f855ad7a4 100644 --- a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::{Expr, PySourceType, Ranged}; +use ruff_python_ast::{Expr, FStringPart, PySourceType, Ranged}; use ruff_python_parser::{lexer, AsMode, StringKind, Tok}; use ruff_text_size::{TextRange, TextSize}; @@ -80,10 +80,14 @@ fn find_useless_f_strings<'a>( } /// F541 -pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) { +pub(crate) fn f_string_missing_placeholders( + expr: &Expr, + values: &[FStringPart], + checker: &mut Checker, +) { if !values .iter() - .any(|value| matches!(value, Expr::FormattedValue(_))) + .any(|value| matches!(value, FStringPart::FormattedValue(_))) { for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator(), checker.source_type) diff --git a/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs index b88987fbae885..5aea0842f4923 100644 --- a/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff/src/rules/pylint/rules/assert_on_string_literal.rs @@ -71,25 +71,21 @@ pub(crate) fn assert_on_string_literal(checker: &mut Checker, test: &Expr) { } _ => {} }, - Expr::FString(ast::ExprFString { values, .. }) => { + Expr::FString(ast::ExprFString { parts, .. }) => { checker.diagnostics.push(Diagnostic::new( AssertOnStringLiteral { - kind: if values.iter().all(|value| match value { - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Str(value, ..) => value.is_empty(), - Constant::Bytes(value) => value.is_empty(), - _ => false, - }, - _ => false, + kind: if parts.iter().all(|part| match part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => { + value.is_empty() + } + ast::FStringPart::FormattedValue(_) => false, }) { Kind::Empty - } else if values.iter().any(|value| match value { - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Str(value, ..) => !value.is_empty(), - Constant::Bytes(value) => !value.is_empty(), - _ => false, - }, - _ => false, + } else if parts.iter().any(|part| match part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => { + !value.is_empty() + } + ast::FStringPart::FormattedValue(_) => false, }) { Kind::NonEmpty } else { diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 482eaad976256..b461fce86a018 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -159,7 +159,6 @@ fn is_allowed_value(expr: &Expr) -> bool { | Expr::GeneratorExp(_) | Expr::Compare(_) | Expr::Call(_) - | Expr::FormattedValue(_) | Expr::FString(_) | Expr::Constant(_) | Expr::Attribute(_) diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index f8957b82b244f..facb50fbd8f62 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -55,12 +55,12 @@ impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion { pub(crate) fn explicit_f_string_type_conversion( checker: &mut Checker, expr: &Expr, - values: &[Expr], + values: &[ast::FStringPart], ) { for (index, formatted_value) in values .iter() .filter_map(|expr| { - if let Expr::FormattedValue(expr) = &expr { + if let ast::FStringPart::FormattedValue(expr) = &expr { Some(expr) } else { None @@ -68,8 +68,10 @@ pub(crate) fn explicit_f_string_type_conversion( }) .enumerate() { - let ast::ExprFormattedValue { - value, conversion, .. + let ast::FormattedValue { + expression, + conversion, + .. } = formatted_value; // Skip if there's already a conversion flag. @@ -86,7 +88,7 @@ pub(crate) fn explicit_f_string_type_conversion( range: _, }, .. - }) = value.as_ref() + }) = expression.as_ref() else { continue; }; @@ -121,7 +123,7 @@ pub(crate) fn explicit_f_string_type_conversion( continue; } - let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range()); + let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, expression.range()); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { convert_call_to_conversion_flag(expr, index, checker.locator(), checker.stylist()) diff --git a/crates/ruff/src/rules/ruff/rules/unreachable.rs b/crates/ruff/src/rules/ruff/rules/unreachable.rs index 180bd84e6c29d..462f0ad3ad99d 100644 --- a/crates/ruff/src/rules/ruff/rules/unreachable.rs +++ b/crates/ruff/src/rules/ruff/rules/unreachable.rs @@ -625,7 +625,6 @@ impl<'stmt> BasicBlocksBuilder<'stmt> { | Expr::Set(_) | Expr::Compare(_) | Expr::Call(_) - | Expr::FormattedValue(_) | Expr::FString(_) | Expr::Constant(_) | Expr::Attribute(_) diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs index aadfa930c49f7..4ab2373826752 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -88,10 +88,12 @@ pub(crate) fn raise_vanilla_args(checker: &mut Checker, expr: &Expr) { /// some whitespace). fn contains_message(expr: &Expr) -> bool { match expr { - Expr::FString(ast::ExprFString { values, .. }) => { - for value in values { - if contains_message(value) { - return true; + Expr::FString(ast::ExprFString { parts, .. }) => { + for part in parts { + if let ast::FStringPart::Literal(ast::PartialString { value, .. }) = part { + if value.chars().any(char::is_whitespace) { + return true; + } } } } diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index cf4fcc95a0709..a91724b21a677 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -517,6 +517,36 @@ impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ComparableFStringPart<'a> { + Literal(&'a str), + FormattedValue(FormattedValue<'a>), +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct FormattedValue<'a> { + expression: ComparableExpr<'a>, + debug_text: Option<&'a ast::DebugText>, + conversion: ast::ConversionFlag, + format_spec: Vec>, +} + +impl<'a> From<&'a ast::FStringPart> for ComparableFStringPart<'a> { + fn from(fstring_part: &'a ast::FStringPart) -> Self { + match fstring_part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => Self::Literal(value), + ast::FStringPart::FormattedValue(formatted_value) => { + Self::FormattedValue(FormattedValue { + expression: (&formatted_value.expression).into(), + debug_text: formatted_value.debug_text.as_ref(), + conversion: formatted_value.conversion, + format_spec: formatted_value.format_spec.iter().map(Into::into).collect(), + }) + } + } + } +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableElifElseClause<'a> { test: Option>, @@ -644,12 +674,12 @@ pub struct ExprFormattedValue<'a> { value: Box>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Option>>, + format_spec: Vec>, } #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprFString<'a> { - values: Vec>, + parts: Vec>, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -885,24 +915,12 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { func: func.into(), arguments: arguments.into(), }), - ast::Expr::FormattedValue(ast::ExprFormattedValue { - value, - conversion, - debug_text, - format_spec, - range: _, - }) => Self::FormattedValue(ExprFormattedValue { - value: value.into(), - conversion: *conversion, - debug_text: debug_text.as_ref(), - format_spec: format_spec.as_ref().map(Into::into), - }), ast::Expr::FString(ast::ExprFString { - values, + parts, implicit_concatenated: _, range: _, }) => Self::FString(ExprFString { - values: values.iter().map(Into::into).collect(), + parts: parts.iter().map(Into::into).collect(), }), ast::Expr::Constant(ast::ExprConstant { value, diff --git a/crates/ruff_python_ast/src/expression.rs b/crates/ruff_python_ast/src/expression.rs index c76b093faba4d..3d1e129b6bcf2 100644 --- a/crates/ruff_python_ast/src/expression.rs +++ b/crates/ruff_python_ast/src/expression.rs @@ -23,7 +23,6 @@ pub enum ExpressionRef<'a> { YieldFrom(&'a ast::ExprYieldFrom), Compare(&'a ast::ExprCompare), Call(&'a ast::ExprCall), - FormattedValue(&'a ast::ExprFormattedValue), FString(&'a ast::ExprFString), Constant(&'a ast::ExprConstant), Attribute(&'a ast::ExprAttribute), @@ -62,7 +61,6 @@ impl<'a> From<&'a Expr> for ExpressionRef<'a> { Expr::YieldFrom(value) => ExpressionRef::YieldFrom(value), Expr::Compare(value) => ExpressionRef::Compare(value), Expr::Call(value) => ExpressionRef::Call(value), - Expr::FormattedValue(value) => ExpressionRef::FormattedValue(value), Expr::FString(value) => ExpressionRef::FString(value), Expr::Constant(value) => ExpressionRef::Constant(value), Expr::Attribute(value) => ExpressionRef::Attribute(value), @@ -162,11 +160,6 @@ impl<'a> From<&'a ast::ExprCall> for ExpressionRef<'a> { Self::Call(value) } } -impl<'a> From<&'a ast::ExprFormattedValue> for ExpressionRef<'a> { - fn from(value: &'a ast::ExprFormattedValue) -> Self { - Self::FormattedValue(value) - } -} impl<'a> From<&'a ast::ExprFString> for ExpressionRef<'a> { fn from(value: &'a ast::ExprFString) -> Self { Self::FString(value) @@ -238,7 +231,6 @@ impl<'a> From> for AnyNodeRef<'a> { ExpressionRef::YieldFrom(expression) => AnyNodeRef::ExprYieldFrom(expression), ExpressionRef::Compare(expression) => AnyNodeRef::ExprCompare(expression), ExpressionRef::Call(expression) => AnyNodeRef::ExprCall(expression), - ExpressionRef::FormattedValue(expression) => AnyNodeRef::ExprFormattedValue(expression), ExpressionRef::FString(expression) => AnyNodeRef::ExprFString(expression), ExpressionRef::Constant(expression) => AnyNodeRef::ExprConstant(expression), ExpressionRef::Attribute(expression) => AnyNodeRef::ExprAttribute(expression), @@ -275,7 +267,6 @@ impl Ranged for ExpressionRef<'_> { ExpressionRef::YieldFrom(expression) => expression.range(), ExpressionRef::Compare(expression) => expression.range(), ExpressionRef::Call(expression) => expression.range(), - ExpressionRef::FormattedValue(expression) => expression.range(), ExpressionRef::FString(expression) => expression.range(), ExpressionRef::Constant(expression) => expression.range(), ExpressionRef::Attribute(expression) => expression.range(), diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 522bdcd19bd5b..e3eb1e3cd8031 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -9,8 +9,8 @@ use ruff_text_size::TextRange; use crate::call_path::CallPath; use crate::statement_visitor::{walk_body, walk_stmt, StatementVisitor}; use crate::{ - self as ast, Arguments, Constant, ExceptHandler, Expr, MatchCase, Pattern, Ranged, Stmt, - TypeParam, + self as ast, Arguments, Constant, ExceptHandler, Expr, FStringPart, MatchCase, Pattern, Ranged, + Stmt, TypeParam, }; /// Return `true` if the `Stmt` is a compound statement (as opposed to a simple statement). @@ -122,10 +122,12 @@ where return true; } match expr { - Expr::BoolOp(ast::ExprBoolOp { values, .. }) - | Expr::FString(ast::ExprFString { values, .. }) => { + Expr::BoolOp(ast::ExprBoolOp { values, .. }) => { values.iter().any(|expr| any_over_expr(expr, func)) } + Expr::FString(ast::ExprFString { parts, .. }) => { + parts.iter().any(|part| any_over_fstring_part(part, func)) + } Expr::NamedExpr(ast::ExprNamedExpr { target, value, @@ -216,14 +218,6 @@ where .iter() .any(|keyword| any_over_expr(&keyword.value, func)) } - Expr::FormattedValue(ast::ExprFormattedValue { - value, format_spec, .. - }) => { - any_over_expr(value, func) - || format_spec - .as_ref() - .is_some_and(|value| any_over_expr(value, func)) - } Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { any_over_expr(value, func) || any_over_expr(slice, func) } @@ -300,6 +294,25 @@ where } } +pub fn any_over_fstring_part(part: &FStringPart, func: &F) -> bool +where + F: Fn(&Expr) -> bool, +{ + match part { + FStringPart::Literal(_) => false, + FStringPart::FormattedValue(ast::FormattedValue { + expression, + format_spec, + .. + }) => { + any_over_expr(expression, func) + || format_spec + .iter() + .any(|spec_part| any_over_fstring_part(spec_part, func)) + } + } +} + pub fn any_over_stmt(stmt: &Stmt, func: &F) -> bool where F: Fn(&Expr) -> bool, @@ -1082,19 +1095,14 @@ impl Truthiness { Constant::Complex { real, imag } => Some(*real != 0.0 || *imag != 0.0), Constant::Ellipsis => Some(true), }, - Expr::FString(ast::ExprFString { values, .. }) => { - if values.is_empty() { + Expr::FString(ast::ExprFString { parts, .. }) => { + if parts.is_empty() { Some(false) - } else if values.iter().any(|value| { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(ast::StringConstant { value, .. }), - .. - }) = &value - { + } else if parts.iter().any(|part| match part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => { !value.is_empty() - } else { - false } + ast::FStringPart::FormattedValue(_) => true, }) { Some(true) } else { diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 988a2164c7467..6a450c4c41bde 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -71,7 +71,6 @@ pub enum AnyNode { ExprYieldFrom(ast::ExprYieldFrom), ExprCompare(ast::ExprCompare), ExprCall(ast::ExprCall), - ExprFormattedValue(ast::ExprFormattedValue), ExprFString(ast::ExprFString), ExprConstant(ast::ExprConstant), ExprAttribute(ast::ExprAttribute), @@ -83,6 +82,8 @@ pub enum AnyNode { ExprSlice(ast::ExprSlice), ExprIpyEscapeCommand(ast::ExprIpyEscapeCommand), ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler), + FormattedValue(ast::FormattedValue), + PartialString(ast::PartialString), PatternMatchValue(ast::PatternMatchValue), PatternMatchSingleton(ast::PatternMatchSingleton), PatternMatchSequence(ast::PatternMatchSequence), @@ -158,7 +159,8 @@ impl AnyNode { | AnyNode::ExprYieldFrom(_) | AnyNode::ExprCompare(_) | AnyNode::ExprCall(_) - | AnyNode::ExprFormattedValue(_) + | AnyNode::FormattedValue(_) + | AnyNode::PartialString(_) | AnyNode::ExprFString(_) | AnyNode::ExprConstant(_) | AnyNode::ExprAttribute(_) @@ -217,7 +219,6 @@ impl AnyNode { AnyNode::ExprYieldFrom(node) => Some(Expr::YieldFrom(node)), AnyNode::ExprCompare(node) => Some(Expr::Compare(node)), AnyNode::ExprCall(node) => Some(Expr::Call(node)), - AnyNode::ExprFormattedValue(node) => Some(Expr::FormattedValue(node)), AnyNode::ExprFString(node) => Some(Expr::FString(node)), AnyNode::ExprConstant(node) => Some(Expr::Constant(node)), AnyNode::ExprAttribute(node) => Some(Expr::Attribute(node)), @@ -257,6 +258,8 @@ impl AnyNode { | AnyNode::StmtContinue(_) | AnyNode::StmtIpyEscapeCommand(_) | AnyNode::ExceptHandlerExceptHandler(_) + | AnyNode::FormattedValue(_) + | AnyNode::PartialString(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -332,7 +335,8 @@ impl AnyNode { | AnyNode::ExprYieldFrom(_) | AnyNode::ExprCompare(_) | AnyNode::ExprCall(_) - | AnyNode::ExprFormattedValue(_) + | AnyNode::FormattedValue(_) + | AnyNode::PartialString(_) | AnyNode::ExprFString(_) | AnyNode::ExprConstant(_) | AnyNode::ExprAttribute(_) @@ -427,7 +431,8 @@ impl AnyNode { | AnyNode::ExprYieldFrom(_) | AnyNode::ExprCompare(_) | AnyNode::ExprCall(_) - | AnyNode::ExprFormattedValue(_) + | AnyNode::FormattedValue(_) + | AnyNode::PartialString(_) | AnyNode::ExprFString(_) | AnyNode::ExprConstant(_) | AnyNode::ExprAttribute(_) @@ -507,7 +512,8 @@ impl AnyNode { | AnyNode::ExprYieldFrom(_) | AnyNode::ExprCompare(_) | AnyNode::ExprCall(_) - | AnyNode::ExprFormattedValue(_) + | AnyNode::FormattedValue(_) + | AnyNode::PartialString(_) | AnyNode::ExprFString(_) | AnyNode::ExprConstant(_) | AnyNode::ExprAttribute(_) @@ -612,7 +618,8 @@ impl AnyNode { Self::ExprYieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), Self::ExprCompare(node) => AnyNodeRef::ExprCompare(node), Self::ExprCall(node) => AnyNodeRef::ExprCall(node), - Self::ExprFormattedValue(node) => AnyNodeRef::ExprFormattedValue(node), + Self::FormattedValue(node) => AnyNodeRef::FormattedValue(node), + Self::PartialString(node) => AnyNodeRef::PartialString(node), Self::ExprFString(node) => AnyNodeRef::ExprFString(node), Self::ExprConstant(node) => AnyNodeRef::ExprConstant(node), Self::ExprAttribute(node) => AnyNodeRef::ExprAttribute(node), @@ -2565,12 +2572,12 @@ impl AstNode for ast::ExprCall { visitor.visit_arguments(arguments); } } -impl AstNode for ast::ExprFormattedValue { +impl AstNode for ast::FormattedValue { fn cast(kind: AnyNode) -> Option where Self: Sized, { - if let AnyNode::ExprFormattedValue(node) = kind { + if let AnyNode::FormattedValue(node) = kind { Some(node) } else { None @@ -2578,7 +2585,7 @@ impl AstNode for ast::ExprFormattedValue { } fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { - if let AnyNodeRef::ExprFormattedValue(node) = kind { + if let AnyNodeRef::FormattedValue(node) = kind { Some(node) } else { None @@ -2597,16 +2604,53 @@ impl AstNode for ast::ExprFormattedValue { where V: PreorderVisitor<'a> + ?Sized, { - let ast::ExprFormattedValue { - value, format_spec, .. + let ast::FormattedValue { + expression, + format_spec, + .. } = self; - visitor.visit_expr(value); + visitor.visit_expr(expression); - if let Some(expr) = format_spec { - visitor.visit_format_spec(expr); + for spec_part in format_spec { + visitor.visit_fstring_part(spec_part); } } } +impl AstNode for ast::PartialString { + fn cast(kind: AnyNode) -> Option + where + Self: Sized, + { + if let AnyNode::PartialString(node) = kind { + Some(node) + } else { + None + } + } + + fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { + if let AnyNodeRef::PartialString(node) = kind { + Some(node) + } else { + None + } + } + + fn as_any_node_ref(&self) -> AnyNodeRef { + AnyNodeRef::from(self) + } + + fn into_any_node(self) -> AnyNode { + AnyNode::from(self) + } + + fn visit_preorder<'a, V>(&'a self, _visitor: &mut V) + where + V: PreorderVisitor<'a> + ?Sized, + { + // TODO: is this correct? + } +} impl AstNode for ast::ExprFString { fn cast(kind: AnyNode) -> Option where @@ -2640,13 +2684,13 @@ impl AstNode for ast::ExprFString { V: PreorderVisitor<'a> + ?Sized, { let ast::ExprFString { - values, + parts, implicit_concatenated: _, range: _, } = self; - for expr in values { - visitor.visit_expr(expr); + for part in parts { + visitor.visit_fstring_part(part); } } } @@ -4133,7 +4177,6 @@ impl From for AnyNode { Expr::YieldFrom(node) => AnyNode::ExprYieldFrom(node), Expr::Compare(node) => AnyNode::ExprCompare(node), Expr::Call(node) => AnyNode::ExprCall(node), - Expr::FormattedValue(node) => AnyNode::ExprFormattedValue(node), Expr::FString(node) => AnyNode::ExprFString(node), Expr::Constant(node) => AnyNode::ExprConstant(node), Expr::Attribute(node) => AnyNode::ExprAttribute(node), @@ -4450,9 +4493,15 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(node: ast::ExprFormattedValue) -> Self { - AnyNode::ExprFormattedValue(node) +impl From for AnyNode { + fn from(node: ast::FormattedValue) -> Self { + AnyNode::FormattedValue(node) + } +} + +impl From for AnyNode { + fn from(node: ast::PartialString) -> Self { + AnyNode::PartialString(node) } } @@ -4702,7 +4751,8 @@ impl Ranged for AnyNode { AnyNode::ExprYieldFrom(node) => node.range(), AnyNode::ExprCompare(node) => node.range(), AnyNode::ExprCall(node) => node.range(), - AnyNode::ExprFormattedValue(node) => node.range(), + AnyNode::FormattedValue(node) => node.range(), + AnyNode::PartialString(node) => node.range(), AnyNode::ExprFString(node) => node.range(), AnyNode::ExprConstant(node) => node.range(), AnyNode::ExprAttribute(node) => node.range(), @@ -4789,7 +4839,8 @@ pub enum AnyNodeRef<'a> { ExprYieldFrom(&'a ast::ExprYieldFrom), ExprCompare(&'a ast::ExprCompare), ExprCall(&'a ast::ExprCall), - ExprFormattedValue(&'a ast::ExprFormattedValue), + FormattedValue(&'a ast::FormattedValue), + PartialString(&'a ast::PartialString), ExprFString(&'a ast::ExprFString), ExprConstant(&'a ast::ExprConstant), ExprAttribute(&'a ast::ExprAttribute), @@ -4875,7 +4926,8 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprYieldFrom(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprCompare(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprCall(node) => NonNull::from(*node).cast(), - AnyNodeRef::ExprFormattedValue(node) => NonNull::from(*node).cast(), + AnyNodeRef::FormattedValue(node) => NonNull::from(*node).cast(), + AnyNodeRef::PartialString(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprFString(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprConstant(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprAttribute(node) => NonNull::from(*node).cast(), @@ -4967,7 +5019,8 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprYieldFrom(_) => NodeKind::ExprYieldFrom, AnyNodeRef::ExprCompare(_) => NodeKind::ExprCompare, AnyNodeRef::ExprCall(_) => NodeKind::ExprCall, - AnyNodeRef::ExprFormattedValue(_) => NodeKind::ExprFormattedValue, + AnyNodeRef::FormattedValue(_) => NodeKind::FormattedValue, + AnyNodeRef::PartialString(_) => NodeKind::PartialString, AnyNodeRef::ExprFString(_) => NodeKind::ExprFString, AnyNodeRef::ExprConstant(_) => NodeKind::ExprConstant, AnyNodeRef::ExprAttribute(_) => NodeKind::ExprAttribute, @@ -5054,7 +5107,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFormattedValue(_) + | AnyNodeRef::FormattedValue(_) + | AnyNodeRef::PartialString(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprConstant(_) | AnyNodeRef::ExprAttribute(_) @@ -5113,7 +5167,6 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFormattedValue(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprConstant(_) | AnyNodeRef::ExprAttribute(_) @@ -5153,6 +5206,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::StmtContinue(_) | AnyNodeRef::StmtIpyEscapeCommand(_) | AnyNodeRef::ExceptHandlerExceptHandler(_) + | AnyNodeRef::FormattedValue(_) + | AnyNodeRef::PartialString(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -5227,7 +5282,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFormattedValue(_) + | AnyNodeRef::FormattedValue(_) + | AnyNodeRef::PartialString(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprConstant(_) | AnyNodeRef::ExprAttribute(_) @@ -5322,7 +5378,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFormattedValue(_) + | AnyNodeRef::FormattedValue(_) + | AnyNodeRef::PartialString(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprConstant(_) | AnyNodeRef::ExprAttribute(_) @@ -5402,7 +5459,8 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFormattedValue(_) + | AnyNodeRef::FormattedValue(_) + | AnyNodeRef::PartialString(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprConstant(_) | AnyNodeRef::ExprAttribute(_) @@ -5516,7 +5574,8 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprYieldFrom(node) => node.visit_preorder(visitor), AnyNodeRef::ExprCompare(node) => node.visit_preorder(visitor), AnyNodeRef::ExprCall(node) => node.visit_preorder(visitor), - AnyNodeRef::ExprFormattedValue(node) => node.visit_preorder(visitor), + AnyNodeRef::FormattedValue(node) => node.visit_preorder(visitor), + AnyNodeRef::PartialString(node) => node.visit_preorder(visitor), AnyNodeRef::ExprFString(node) => node.visit_preorder(visitor), AnyNodeRef::ExprConstant(node) => node.visit_preorder(visitor), AnyNodeRef::ExprAttribute(node) => node.visit_preorder(visitor), @@ -5827,9 +5886,15 @@ impl<'a> From<&'a ast::ExprCall> for AnyNodeRef<'a> { } } -impl<'a> From<&'a ast::ExprFormattedValue> for AnyNodeRef<'a> { - fn from(node: &'a ast::ExprFormattedValue) -> Self { - AnyNodeRef::ExprFormattedValue(node) +impl<'a> From<&'a ast::FormattedValue> for AnyNodeRef<'a> { + fn from(node: &'a ast::FormattedValue) -> Self { + AnyNodeRef::FormattedValue(node) + } +} + +impl<'a> From<&'a ast::PartialString> for AnyNodeRef<'a> { + fn from(node: &'a ast::PartialString) -> Self { + AnyNodeRef::PartialString(node) } } @@ -6040,7 +6105,6 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> { Expr::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), Expr::Compare(node) => AnyNodeRef::ExprCompare(node), Expr::Call(node) => AnyNodeRef::ExprCall(node), - Expr::FormattedValue(node) => AnyNodeRef::ExprFormattedValue(node), Expr::FString(node) => AnyNodeRef::ExprFString(node), Expr::Constant(node) => AnyNodeRef::ExprConstant(node), Expr::Attribute(node) => AnyNodeRef::ExprAttribute(node), @@ -6192,7 +6256,8 @@ impl Ranged for AnyNodeRef<'_> { AnyNodeRef::ExprYieldFrom(node) => node.range(), AnyNodeRef::ExprCompare(node) => node.range(), AnyNodeRef::ExprCall(node) => node.range(), - AnyNodeRef::ExprFormattedValue(node) => node.range(), + AnyNodeRef::FormattedValue(node) => node.range(), + AnyNodeRef::PartialString(node) => node.range(), AnyNodeRef::ExprFString(node) => node.range(), AnyNodeRef::ExprConstant(node) => node.range(), AnyNodeRef::ExprAttribute(node) => node.range(), @@ -6281,7 +6346,8 @@ pub enum NodeKind { ExprYieldFrom, ExprCompare, ExprCall, - ExprFormattedValue, + FormattedValue, + PartialString, ExprFString, ExprConstant, ExprAttribute, diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index c02d00f9f79ae..9b13f392a031f 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -532,8 +532,6 @@ pub enum Expr { Compare(ExprCompare), #[is(name = "call_expr")] Call(ExprCall), - #[is(name = "formatted_value_expr")] - FormattedValue(ExprFormattedValue), #[is(name = "f_string_expr")] FString(ExprFString), #[is(name = "constant_expr")] @@ -811,18 +809,18 @@ impl From for Expr { /// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue) #[derive(Clone, Debug, PartialEq)] -pub struct ExprFormattedValue { +pub struct FormattedValue { pub range: TextRange, - pub value: Box, + pub expression: Box, pub debug_text: Option, pub conversion: ConversionFlag, - pub format_spec: Option>, + pub format_spec: Vec, } -impl From for Expr { - fn from(payload: ExprFormattedValue) -> Self { - Expr::FormattedValue(payload) - } +#[derive(Clone, Debug, PartialEq)] +pub struct PartialString { + pub range: TextRange, + pub value: String, } /// Transforms a value prior to formatting it. @@ -864,9 +862,9 @@ pub struct DebugText { #[derive(Clone, Debug, PartialEq)] pub struct ExprFString { pub range: TextRange, - pub values: Vec, /// Whether the f-string contains multiple string tokens that were implicitly concatenated. pub implicit_concatenated: bool, + pub parts: Vec, } impl From for Expr { @@ -875,6 +873,12 @@ impl From for Expr { } } +#[derive(Clone, Debug, PartialEq)] +pub enum FStringPart { + Literal(PartialString), + FormattedValue(FormattedValue), +} + /// See also [Constant](https://docs.python.org/3/library/ast.html#ast.Constant) #[derive(Clone, Debug, PartialEq)] pub struct ExprConstant { @@ -2837,11 +2841,6 @@ impl Ranged for crate::nodes::ExprCall { self.range } } -impl Ranged for crate::nodes::ExprFormattedValue { - fn range(&self) -> TextRange { - self.range - } -} impl Ranged for crate::nodes::ExprFString { fn range(&self) -> TextRange { self.range @@ -2912,7 +2911,6 @@ impl Ranged for crate::Expr { Self::YieldFrom(node) => node.range(), Self::Compare(node) => node.range(), Self::Call(node) => node.range(), - Self::FormattedValue(node) => node.range(), Self::FString(node) => node.range(), Self::Constant(node) => node.range(), Self::Attribute(node) => node.range(), @@ -3100,3 +3098,24 @@ mod size_assertions { assert_eq_size!(Pattern, [u8; 96]); assert_eq_size!(Mod, [u8; 32]); } + +impl Ranged for crate::nodes::FormattedValue { + fn range(&self) -> TextRange { + self.range + } +} + +impl Ranged for crate::nodes::FStringPart { + fn range(&self) -> TextRange { + match self { + FStringPart::Literal(node) => node.range(), + FStringPart::FormattedValue(node) => node.range(), + } + } +} + +impl Ranged for crate::nodes::PartialString { + fn range(&self) -> TextRange { + self.range + } +} diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 122cdbc259ba0..e6712f859dda0 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -128,22 +128,10 @@ pub fn relocate_expr(expr: &mut Expr, location: TextRange) { relocate_keyword(keyword, location); } } - Expr::FormattedValue(nodes::ExprFormattedValue { - value, - format_spec, - range, - .. - }) => { + Expr::FString(nodes::ExprFString { parts, range, .. }) => { *range = location; - relocate_expr(value, location); - if let Some(expr) = format_spec { - relocate_expr(expr, location); - } - } - Expr::FString(nodes::ExprFString { values, range, .. }) => { - *range = location; - for expr in values { - relocate_expr(expr, location); + for part in parts { + relocate_fstring_part(part, location); } } Expr::Constant(nodes::ExprConstant { range, .. }) => { @@ -204,3 +192,25 @@ pub fn relocate_expr(expr: &mut Expr, location: TextRange) { } } } + +/// Change a f-string part's location (recursively) to match a desired, fixed +/// location. +fn relocate_fstring_part(part: &mut nodes::FStringPart, location: TextRange) { + match part { + nodes::FStringPart::Literal(nodes::PartialString { range, .. }) => { + *range = location; + } + nodes::FStringPart::FormattedValue(nodes::FormattedValue { + range, + expression, + format_spec, + .. + }) => { + *range = location; + relocate_expr(expression, location); + for spec_part in format_spec { + relocate_fstring_part(spec_part, location); + } + } + } +} diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 7d30a76e0815a..e5165111c36c9 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -4,9 +4,9 @@ pub mod preorder; use crate::{ self as ast, Alias, Arguments, BoolOp, CmpOp, Comprehension, Decorator, ElifElseClause, - ExceptHandler, Expr, ExprContext, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, - PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamTypeVar, TypeParams, UnaryOp, - WithItem, + ExceptHandler, Expr, ExprContext, FStringPart, Keyword, MatchCase, Operator, Parameter, + Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamTypeVar, + TypeParams, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. @@ -50,9 +50,6 @@ pub trait Visitor<'a> { fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { walk_except_handler(self, except_handler); } - fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_format_spec(self, format_spec); - } fn visit_arguments(&mut self, arguments: &'a Arguments) { walk_arguments(self, arguments); } @@ -95,6 +92,9 @@ pub trait Visitor<'a> { fn visit_elif_else_clause(&mut self, elif_else_clause: &'a ElifElseClause) { walk_elif_else_clause(self, elif_else_clause); } + fn visit_fstring_part(&mut self, fstring_part: &'a FStringPart) { + walk_fstring_part(self, fstring_part); + } } pub fn walk_body<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, body: &'a [Stmt]) { @@ -464,17 +464,9 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { visitor.visit_expr(func); visitor.visit_arguments(arguments); } - Expr::FormattedValue(ast::ExprFormattedValue { - value, format_spec, .. - }) => { - visitor.visit_expr(value); - if let Some(expr) = format_spec { - visitor.visit_format_spec(expr); - } - } - Expr::FString(ast::ExprFString { values, .. }) => { - for expr in values { - visitor.visit_expr(expr); + Expr::FString(ast::ExprFString { parts, .. }) => { + for part in parts { + visitor.visit_fstring_part(part); } } Expr::Constant(_) => {} @@ -568,10 +560,6 @@ pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( } } -pub fn walk_format_spec<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, format_spec: &'a Expr) { - visitor.visit_expr(format_spec); -} - pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { for arg in &arguments.args { visitor.visit_expr(arg); @@ -718,6 +706,23 @@ pub fn walk_pattern_keyword<'a, V: Visitor<'a> + ?Sized>( visitor.visit_pattern(&pattern_keyword.pattern); } +pub fn walk_fstring_part<'a, V: Visitor<'a> + ?Sized>( + visitor: &mut V, + fstring_part: &'a FStringPart, +) { + if let ast::FStringPart::FormattedValue(ast::FormattedValue { + expression, + format_spec, + .. + }) = fstring_part + { + visitor.visit_expr(expression); + for spec_part in format_spec { + walk_fstring_part(visitor, spec_part); + } + } +} + #[allow(unused_variables)] pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(visitor: &V, expr_context: &'a ExprContext) {} diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 6a64d1daa3a87..04c9b182ca616 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -1,9 +1,9 @@ use crate::node::{AnyNodeRef, AstNode}; use crate::{ Alias, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ElifElseClause, - ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Parameter, ParameterWithDefault, - Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParams, UnaryOp, - WithItem, + ExceptHandler, Expr, FStringPart, FormattedValue, Keyword, MatchCase, Mod, Operator, Parameter, + ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, TypeParam, + TypeParams, UnaryOp, WithItem, }; /// Visitor that traverses all nodes recursively in pre-order. @@ -74,11 +74,6 @@ pub trait PreorderVisitor<'a> { walk_except_handler(self, except_handler); } - #[inline] - fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_format_spec(self, format_spec); - } - #[inline] fn visit_arguments(&mut self, arguments: &'a Arguments) { walk_arguments(self, arguments); @@ -153,6 +148,11 @@ pub trait PreorderVisitor<'a> { fn visit_elif_else_clause(&mut self, elif_else_clause: &'a ElifElseClause) { walk_elif_else_clause(self, elif_else_clause); } + + #[inline] + fn visit_fstring_part(&mut self, fstring_part: &'a FStringPart) { + walk_fstring_part(self, fstring_part); + } } pub fn walk_module<'a, V>(visitor: &mut V, module: &'a Mod) @@ -275,7 +275,6 @@ where Expr::YieldFrom(expr) => expr.visit_preorder(visitor), Expr::Compare(expr) => expr.visit_preorder(visitor), Expr::Call(expr) => expr.visit_preorder(visitor), - Expr::FormattedValue(expr) => expr.visit_preorder(visitor), Expr::FString(expr) => expr.visit_preorder(visitor), Expr::Constant(expr) => expr.visit_preorder(visitor), Expr::Attribute(expr) => expr.visit_preorder(visitor), @@ -499,6 +498,23 @@ where visitor.leave_node(node); } +pub fn walk_fstring_part<'a, V: PreorderVisitor<'a> + ?Sized>( + visitor: &mut V, + fstring_part: &'a FStringPart, +) { + if let FStringPart::FormattedValue(FormattedValue { + expression, + format_spec, + .. + }) = fstring_part + { + visitor.visit_expr(expression); + for spec_part in format_spec { + walk_fstring_part(visitor, spec_part); + } + } +} + pub fn walk_bool_op<'a, V>(_visitor: &mut V, _bool_op: &'a BoolOp) where V: PreorderVisitor<'a> + ?Sized, diff --git a/crates/ruff_python_ast/tests/preorder.rs b/crates/ruff_python_ast/tests/preorder.rs index 8c6cba2f30250..663a204ecfa58 100644 --- a/crates/ruff_python_ast/tests/preorder.rs +++ b/crates/ruff_python_ast/tests/preorder.rs @@ -225,12 +225,6 @@ impl PreorderVisitor<'_> for RecordVisitor { self.exit_node(); } - fn visit_format_spec(&mut self, format_spec: &Expr) { - self.enter_node(format_spec); - walk_expr(self, format_spec); - self.exit_node(); - } - fn visit_parameters(&mut self, parameters: &Parameters) { self.enter_node(parameters); walk_parameters(self, parameters); diff --git a/crates/ruff_python_ast/tests/visitor.rs b/crates/ruff_python_ast/tests/visitor.rs index b44cf55ce0324..4d733eb57436b 100644 --- a/crates/ruff_python_ast/tests/visitor.rs +++ b/crates/ruff_python_ast/tests/visitor.rs @@ -228,12 +228,6 @@ impl Visitor<'_> for RecordVisitor { self.exit_node(); } - fn visit_format_spec(&mut self, format_spec: &Expr) { - self.enter_node(format_spec); - walk_expr(self, format_spec); - self.exit_node(); - } - fn visit_parameters(&mut self, parameters: &Parameters) { self.enter_node(parameters); walk_parameters(self, parameters); diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 4408ff9f05d5e..e3d7861e646ae 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -1064,20 +1064,8 @@ impl<'a> Generator<'a> { } self.p(")"); } - Expr::FormattedValue(ast::ExprFormattedValue { - value, - debug_text, - conversion, - format_spec, - range: _, - }) => self.unparse_formatted( - value, - debug_text.as_ref(), - *conversion, - format_spec.as_deref(), - ), - Expr::FString(ast::ExprFString { values, .. }) => { - self.unparse_f_string(values, false); + Expr::FString(ast::ExprFString { parts, .. }) => { + self.unparse_fstring(parts, false); } Expr::Constant(ast::ExprConstant { value, @@ -1261,9 +1249,9 @@ impl<'a> Generator<'a> { } } - fn unparse_f_string_body(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_fstring_body(&mut self, values: &[ast::FStringPart]) { for value in values { - self.unparse_f_string_elem(value, is_spec); + self.unparse_fstring_elem(value); } } @@ -1272,7 +1260,7 @@ impl<'a> Generator<'a> { val: &Expr, debug_text: Option<&DebugText>, conversion: ConversionFlag, - spec: Option<&Expr>, + spec: &[ast::FStringPart], ) { let mut generator = Generator::new(self.indent, self.quote, self.line_ending); generator.unparse_expr(val, precedence::FORMATTED_VALUE); @@ -1300,50 +1288,37 @@ impl<'a> Generator<'a> { self.p(&format!("{}", conversion as u8 as char)); } - if let Some(spec) = spec { + if !spec.is_empty() { self.p(":"); - self.unparse_f_string_elem(spec, true); + self.unparse_fstring(spec, true); } self.p("}"); } - fn unparse_f_string_elem(&mut self, expr: &Expr, is_spec: bool) { - match expr { - Expr::Constant(ast::ExprConstant { value, .. }) => { - if let Constant::Str(ast::StringConstant { value, .. }) = value { - self.unparse_f_string_literal(value); - } else { - unreachable!() - } - } - Expr::FString(ast::ExprFString { values, .. }) => { - self.unparse_f_string(values, is_spec); + fn unparse_fstring_elem(&mut self, part: &ast::FStringPart) { + match part { + ast::FStringPart::Literal(ast::PartialString { value, .. }) => { + self.unparse_fstring_literal(value); } - Expr::FormattedValue(ast::ExprFormattedValue { - value, + ast::FStringPart::FormattedValue(ast::FormattedValue { + expression, debug_text, conversion, format_spec, range: _, - }) => self.unparse_formatted( - value, - debug_text.as_ref(), - *conversion, - format_spec.as_deref(), - ), - _ => unreachable!(), + }) => self.unparse_formatted(expression, debug_text.as_ref(), *conversion, format_spec), } } - fn unparse_f_string_literal(&mut self, s: &str) { + fn unparse_fstring_literal(&mut self, s: &str) { let s = s.replace('{', "{{").replace('}', "}}"); self.p(&s); } - fn unparse_f_string(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_fstring(&mut self, values: &[ast::FStringPart], is_spec: bool) { if is_spec { - self.unparse_f_string_body(values, is_spec); + self.unparse_fstring_body(values); } else { self.p("f"); let mut generator = Generator::new( @@ -1354,7 +1329,7 @@ impl<'a> Generator<'a> { }, self.line_ending, ); - generator.unparse_f_string_body(values, is_spec); + generator.unparse_fstring_body(values); let body = &generator.buffer; self.p_str_repr(body); } @@ -1659,7 +1634,7 @@ class Foo: } #[test] - fn self_documenting_f_string() { + fn self_documenting_fstring() { assert_round_trip!(r#"f"{ chr(65) = }""#); assert_round_trip!(r#"f"{ chr(65) = !s}""#); assert_round_trip!(r#"f"{ chr(65) = !r}""#); diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index a60efa1cddfa8..23b2c2d726bbf 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -3,7 +3,7 @@ f'{two}' ) - +# quote handling for raw f-strings rf"Not-so-tricky \"quote" # Regression test for fstrings dropping comments @@ -33,3 +33,61 @@ # comment '' ) + + +f"{ chr(65) = }" +f"{ chr(65) = !s}" +f"{ chr(65) = !r}" +f"{ chr(65) = :#x}" +f"{a=!r:0.05f}" + +f"{ {} = }" +f"{ {}=}" + +# should add some nice spaces +f"{1-2+3}" + + +# don't switch quotes inside a formatted value +f"\"{f'{nested} inner'}\" outer" + +# need a space to avoid escaping the curly +f"{ {1}}" + +# extra spaces don't interfere with debug_text +f"{ {1}=}" + +# handle string continuations +( + '' + f'"{1}' +) + +# it's ok to change triple quotes with even with qoutes inside f-strings +f''' {""} ''' + +# it's ok to change the inner single to a double quote +f""" {f' {1}'} """ +f''' {f' {1}'} ''' + +# TODO: would we want to swap the quotes to use double on the outside? +f' {f" {1}"} ' +f''' {f""" {1}"""} ''' + +# various nested quotes to leave unchanged +f" {f' {1}'} " +f""" {f" {1}"} """ +f""" {f''' {1}'''} """ +f"{f'''{'nested'} inner'''} outer" + + +# must not break lines inside non-triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" + +) +# TODO: ok to ot break lines inside triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" + +) diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 01caed9e6854d..bea6027184b37 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,16 +1,32 @@ use crate::comments::Comments; +use crate::expression::string::StringQuotes; use crate::PyFormatOptions; use ruff_formatter::{Buffer, FormatContext, GroupId, SourceCode}; use ruff_source_file::Locator; use std::fmt::{Debug, Formatter}; use std::ops::{Deref, DerefMut}; +#[derive(Clone, Copy)] +pub(crate) struct SurroundingFStringQuotes { + pub(crate) closest: StringQuotes, + pub(crate) all_triple: bool, +} + +#[derive(Clone, Copy, Default)] +// TODO: names +pub(crate) enum InsideFormattedValue { + #[default] + Outside, + Inside(SurroundingFStringQuotes), +} + #[derive(Clone)] pub struct PyFormatContext<'a> { options: PyFormatOptions, contents: &'a str, comments: Comments<'a>, node_level: NodeLevel, + inside_formatted_value: InsideFormattedValue, } impl<'a> PyFormatContext<'a> { @@ -20,6 +36,7 @@ impl<'a> PyFormatContext<'a> { contents, comments, node_level: NodeLevel::TopLevel, + inside_formatted_value: InsideFormattedValue::default(), } } @@ -40,6 +57,14 @@ impl<'a> PyFormatContext<'a> { self.node_level } + pub(crate) fn set_inside_formatted_value(&mut self, inside: InsideFormattedValue) { + self.inside_formatted_value = inside; + } + + pub(crate) fn inside_formatted_value(&self) -> InsideFormattedValue { + self.inside_formatted_value + } + pub(crate) fn comments(&self) -> &Comments<'a> { &self.comments } @@ -153,3 +178,72 @@ where .set_node_level(self.saved_level); } } + +pub(crate) struct WithInsideFormattedValue<'ast, 'buf, B> +where + B: Buffer>, +{ + buffer: &'buf mut B, + saved_value: InsideFormattedValue, +} + +impl<'ast, 'buf, B> WithInsideFormattedValue<'ast, 'buf, B> +where + B: Buffer>, +{ + pub(crate) fn new(buffer: &'buf mut B, quotes: StringQuotes) -> Self { + let context = buffer.state_mut().context_mut(); + let saved_value = context.inside_formatted_value(); + + let all_triple = match saved_value { + InsideFormattedValue::Outside => quotes.is_triple(), + InsideFormattedValue::Inside(SurroundingFStringQuotes { all_triple, .. }) => { + all_triple && quotes.is_triple() + } + }; + + let new = SurroundingFStringQuotes { + closest: quotes, + all_triple, + }; + + context.set_inside_formatted_value(InsideFormattedValue::Inside(new)); + + Self { + buffer, + saved_value, + } + } +} + +impl<'ast, 'buf, B> Deref for WithInsideFormattedValue<'ast, 'buf, B> +where + B: Buffer>, +{ + type Target = B; + + fn deref(&self) -> &Self::Target { + self.buffer + } +} + +impl<'ast, 'buf, B> DerefMut for WithInsideFormattedValue<'ast, 'buf, B> +where + B: Buffer>, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + self.buffer + } +} + +impl<'ast, B> Drop for WithInsideFormattedValue<'ast, '_, B> +where + B: Buffer>, +{ + fn drop(&mut self) { + self.buffer + .state_mut() + .context_mut() + .set_inside_formatted_value(self.saved_value); + } +} diff --git a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs deleted file mode 100644 index 5133cb86e5845..0000000000000 --- a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::ExprFormattedValue; - -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; -use crate::prelude::*; - -#[derive(Default)] -pub struct FormatExprFormattedValue; - -impl FormatNodeRule for FormatExprFormattedValue { - fn fmt_fields(&self, _item: &ExprFormattedValue, _f: &mut PyFormatter) -> FormatResult<()> { - unreachable!("Handled inside of `FormatExprFString"); - } -} - -impl NeedsParentheses for ExprFormattedValue { - fn needs_parentheses( - &self, - _parent: AnyNodeRef, - _context: &PyFormatContext, - ) -> OptionalParentheses { - OptionalParentheses::Multiline - } -} diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 52255ec4f4611..8f57341e2091d 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -29,7 +29,6 @@ pub(crate) mod expr_constant; pub(crate) mod expr_dict; pub(crate) mod expr_dict_comp; pub(crate) mod expr_f_string; -pub(crate) mod expr_formatted_value; pub(crate) mod expr_generator_exp; pub(crate) mod expr_if_exp; pub(crate) mod expr_ipy_escape_command; @@ -87,7 +86,6 @@ impl FormatRule> for FormatExpr { Expr::YieldFrom(expr) => expr.format().fmt(f), Expr::Compare(expr) => expr.format().fmt(f), Expr::Call(expr) => expr.format().fmt(f), - Expr::FormattedValue(expr) => expr.format().fmt(f), Expr::FString(expr) => expr.format().fmt(f), Expr::Constant(expr) => expr.format().fmt(f), Expr::Attribute(expr) => expr.format().fmt(f), @@ -326,7 +324,6 @@ impl NeedsParentheses for Expr { Expr::YieldFrom(expr) => expr.needs_parentheses(parent, context), Expr::Compare(expr) => expr.needs_parentheses(parent, context), Expr::Call(expr) => expr.needs_parentheses(parent, context), - Expr::FormattedValue(expr) => expr.needs_parentheses(parent, context), Expr::FString(expr) => expr.needs_parentheses(parent, context), Expr::Constant(expr) => expr.needs_parentheses(parent, context), Expr::Attribute(expr) => expr.needs_parentheses(parent, context), @@ -527,7 +524,6 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { | Expr::Await(_) | Expr::Yield(_) | Expr::YieldFrom(_) - | Expr::FormattedValue(_) | Expr::FString(_) | Expr::Constant(_) | Expr::Starred(_) diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 49319f9389582..a19e7dea5b2c8 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use bitflags::bitflags; -use ruff_formatter::{format_args, write, FormatError, FormatOptions, TabWidth}; +use ruff_formatter::{ + format_args, write, FormatError, FormatOptions, RemoveSoftLinesBuffer, TabWidth, +}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::{self as ast, ExprConstant, ExprFString, Ranged}; use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; @@ -11,10 +13,10 @@ use ruff_source_file::Locator; use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::comments::{leading_comments, trailing_comments}; +use crate::context::{InsideFormattedValue, SurroundingFStringQuotes, WithInsideFormattedValue}; use crate::expression::parentheses::{ in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, }; -use crate::expression::Expr; use crate::prelude::*; use crate::QuoteStyle; @@ -33,13 +35,24 @@ impl<'a> AnyString<'a> { fn quoting(&self, locator: &Locator) -> Quoting { match self { Self::Constant(_) => Quoting::CanChange, - Self::FString(f_string) => { - if f_string.values.iter().any(|value| match value { - Expr::FormattedValue(ast::ExprFormattedValue { range, .. }) => { + Self::FString(ExprFString { parts, range, .. }) => { + let string_content = locator.slice(*range); + let prefix = StringPrefix::parse(string_content); + let after_prefix = &string_content[usize::from(prefix.text_len())..]; + + let quotes = StringQuotes::parse(after_prefix) + .expect("Didn't find string quotes after prefix"); + + if parts.iter().any(|value| match value { + ast::FStringPart::FormattedValue(ast::FormattedValue { range, .. }) => { let string_content = locator.slice(*range); - string_content.contains(['"', '\'']) + if quotes.triple { + string_content.contains(r#"""""#) || string_content.contains("'''") + } else { + string_content.contains(['"', '\'']) + } } - _ => false, + ast::FStringPart::Literal(_) => false, }) { Quoting::Preserve } else { @@ -121,6 +134,8 @@ impl<'a> Format> for FormatString<'a> { self.string.quoting(&f.context().locator()), &f.context().locator(), f.options().quote_style(), + f.context().inside_formatted_value(), + self.string, ) .fmt(f) } @@ -132,6 +147,8 @@ impl<'a> Format> for FormatString<'a> { Quoting::CanChange, &f.context().locator(), f.options().quote_style(), + f.context().inside_formatted_value(), + self.string, ); format_docstring(&string_part, f) } @@ -160,6 +177,7 @@ impl Format> for FormatStringContinuation<'_> { let comments = f.context().comments().clone(); let locator = f.context().locator(); let quote_style = f.options().quote_style(); + let inside_formatted_value = f.context().inside_formatted_value(); let mut dangling_comments = comments.dangling(self.string); let string_range = self.string.range(); @@ -241,6 +259,8 @@ impl Format> for FormatStringContinuation<'_> { self.string.quoting(&locator), &locator, quote_style, + inside_formatted_value, + self.string, ), trailing_comments(trailing_part_comments) ]); @@ -262,21 +282,29 @@ impl Format> for FormatStringContinuation<'_> { } } -struct FormatStringPart { +struct FormatStringPart<'a> { prefix: StringPrefix, preferred_quotes: StringQuotes, range: TextRange, is_raw_string: bool, + string: &'a AnyString<'a>, } -impl Ranged for FormatStringPart { +impl Ranged for FormatStringPart<'_> { fn range(&self) -> TextRange { self.range } } -impl FormatStringPart { - fn new(range: TextRange, quoting: Quoting, locator: &Locator, quote_style: QuoteStyle) -> Self { +impl<'a> FormatStringPart<'a> { + fn new( + range: TextRange, + quoting: Quoting, + locator: &Locator, + quote_style: QuoteStyle, + inside_formatted_value: InsideFormattedValue, + string: &'a AnyString<'a>, + ) -> Self { let string_content = locator.slice(range); let prefix = StringPrefix::parse(string_content); @@ -292,6 +320,18 @@ impl FormatStringPart { let raw_content = &string_content[relative_raw_content_range]; let is_raw_string = prefix.is_raw_string(); + + let quoting = match inside_formatted_value { + InsideFormattedValue::Inside(SurroundingFStringQuotes { all_triple, .. }) => { + if all_triple && !quotes.triple { + quoting + } else { + Quoting::Preserve + } + } + InsideFormattedValue::Outside => quoting, + }; + let preferred_quotes = match quoting { Quoting::Preserve => quotes, Quoting::CanChange => { @@ -308,11 +348,12 @@ impl FormatStringPart { range: raw_content_range, preferred_quotes, is_raw_string, + string, } } } -impl Format> for FormatStringPart { +impl Format> for FormatStringPart<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let (normalized, contains_newlines) = normalize_string( f.context().locator().slice(self.range), @@ -321,12 +362,42 @@ impl Format> for FormatStringPart { ); write!(f, [self.prefix, self.preferred_quotes])?; - match normalized { - Cow::Borrowed(_) => { - source_text_slice(self.range(), contains_newlines).fmt(f)?; - } - Cow::Owned(normalized) => { - dynamic_text(&normalized, Some(self.start())).fmt(f)?; + if let AnyString::FString(f_string) = self.string { + let joined = format_with(|f: &mut PyFormatter| { + let locator = f.context().locator(); + let mut f = WithInsideFormattedValue::new(f, self.preferred_quotes); + let mut joiner = f.join(); + for part in &f_string.parts { + if let Some(intersection) = part.range().intersect(self.range) { + match part { + ast::FStringPart::Literal(_) => { + let string_content = locator.slice(intersection); + let (normalized, _contains_newlines) = + normalize_string(string_content, self.preferred_quotes, false); + joiner.entry(&dynamic_text(&normalized, None)); + } + ast::FStringPart::FormattedValue(formatted_value) => { + joiner.entry(&formatted_value.format()); + } + } + } + } + joiner.finish() + }); + match f.context().inside_formatted_value() { + InsideFormattedValue::Outside => { + write!(&mut RemoveSoftLinesBuffer::new(f), [joined])?; + } + InsideFormattedValue::Inside(_) => joined.fmt(f)?, + }; + } else { + match normalized { + Cow::Borrowed(_) => { + source_text_slice(self.range(), contains_newlines).fmt(f)?; + } + Cow::Owned(normalized) => { + dynamic_text(&normalized, Some(self.start())).fmt(f)?; + } } } self.preferred_quotes.fmt(f) @@ -557,7 +628,7 @@ fn preferred_quotes( } #[derive(Copy, Clone, Debug)] -pub(super) struct StringQuotes { +pub(crate) struct StringQuotes { triple: bool, style: QuoteStyle, } @@ -574,7 +645,7 @@ impl StringQuotes { Some(Self { triple, style }) } - pub(super) const fn is_triple(self) -> bool { + pub(crate) const fn is_triple(self) -> bool { self.triple } @@ -604,7 +675,7 @@ impl Format> for StringQuotes { /// with the provided `style`. /// /// Returns the normalized string and whether it contains new lines. -fn normalize_string( +pub(crate) fn normalize_string( input: &str, quotes: StringQuotes, is_raw: bool, diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index 08c248e221e5c..fbfb5cad58689 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1534,42 +1534,6 @@ impl<'ast> IntoFormat> for ast::ExprCall { } } -impl FormatRule> - for crate::expression::expr_formatted_value::FormatExprFormattedValue -{ - #[inline] - fn fmt(&self, node: &ast::ExprFormattedValue, f: &mut PyFormatter) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) - } -} -impl<'ast> AsFormat> for ast::ExprFormattedValue { - type Format<'a> = FormatRefWithRule< - 'a, - ast::ExprFormattedValue, - crate::expression::expr_formatted_value::FormatExprFormattedValue, - PyFormatContext<'ast>, - >; - fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), - ) - } -} -impl<'ast> IntoFormat> for ast::ExprFormattedValue { - type Format = FormatOwnedWithRule< - ast::ExprFormattedValue, - crate::expression::expr_formatted_value::FormatExprFormattedValue, - PyFormatContext<'ast>, - >; - fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), - ) - } -} - impl FormatRule> for crate::expression::expr_f_string::FormatExprFString { @@ -1968,6 +1932,78 @@ impl<'ast> IntoFormat> for ast::ExceptHandlerExceptHandler } } +impl FormatRule> + for crate::other::formatted_value::FormatFormattedValue +{ + #[inline] + fn fmt(&self, node: &ast::FormattedValue, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::FormattedValue { + type Format<'a> = FormatRefWithRule< + 'a, + ast::FormattedValue, + crate::other::formatted_value::FormatFormattedValue, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::other::formatted_value::FormatFormattedValue::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::FormattedValue { + type Format = FormatOwnedWithRule< + ast::FormattedValue, + crate::other::formatted_value::FormatFormattedValue, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::other::formatted_value::FormatFormattedValue::default(), + ) + } +} + +impl FormatRule> + for crate::other::partial_string::FormatPartialString +{ + #[inline] + fn fmt(&self, node: &ast::PartialString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::PartialString { + type Format<'a> = FormatRefWithRule< + 'a, + ast::PartialString, + crate::other::partial_string::FormatPartialString, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::other::partial_string::FormatPartialString::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::PartialString { + type Format = FormatOwnedWithRule< + ast::PartialString, + crate::other::partial_string::FormatPartialString, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::other::partial_string::FormatPartialString::default(), + ) + } +} + impl FormatRule> for crate::pattern::pattern_match_value::FormatPatternMatchValue { diff --git a/crates/ruff_python_formatter/src/other/formatted_value.rs b/crates/ruff_python_formatter/src/other/formatted_value.rs new file mode 100644 index 0000000000000..a05bbe2166172 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/formatted_value.rs @@ -0,0 +1,72 @@ +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::prelude::{dynamic_text, format_with, space, text}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::{ConversionFlag, Expr, FormattedValue}; + +#[derive(Default)] +pub struct FormatFormattedValue; + +impl FormatNodeRule for FormatFormattedValue { + fn fmt_fields(&self, item: &FormattedValue, f: &mut PyFormatter) -> FormatResult<()> { + let FormattedValue { + range: _, + expression, + debug_text, + conversion, + format_spec, + } = item; + + let conversion_text = match conversion { + ConversionFlag::Repr => Some("!r"), + ConversionFlag::Str => Some("!s"), + ConversionFlag::Ascii => Some("!a"), + ConversionFlag::None => None, + }; + + // if expression starts with a `{`, we need a space to avoid turning into a literal curly + // with `{{` + let extra_space = if debug_text.is_some() { + // debug_text preserves spaces so no need for any extra + None + } else { + match **expression { + Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) => { + Some(space()) + } + _ => None, + } + }; + + let formatted_format_spec = format_with(|f| f.join().entries(format_spec.iter()).finish()); + + write!( + f, + [ + text("{"), + debug_text.as_ref().map(|d| dynamic_text(&d.leading, None)), + extra_space, + expression.format(), + // not strictly needed here but makes for nicer symmetry + extra_space, + debug_text.as_ref().map(|d| dynamic_text(&d.trailing, None)), + conversion_text.map(text), + (!format_spec.is_empty()).then_some(text(":")), + formatted_format_spec, + text("}") + ] + ) + } +} + +impl NeedsParentheses for FormattedValue { + fn needs_parentheses( + &self, + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline + } +} diff --git a/crates/ruff_python_formatter/src/other/fstring_part.rs b/crates/ruff_python_formatter/src/other/fstring_part.rs new file mode 100644 index 0000000000000..b206098abf8ad --- /dev/null +++ b/crates/ruff_python_formatter/src/other/fstring_part.rs @@ -0,0 +1,29 @@ +use crate::context::{InsideFormattedValue, SurroundingFStringQuotes}; +use crate::expression::string::normalize_string; +use crate::prelude::*; +use crate::AsFormat; +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::{FStringPart, PartialString}; + +impl Format> for FStringPart { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + match self { + FStringPart::Literal(PartialString { value: _, range }) => { + let preferred_quotes = match f.context().inside_formatted_value() { + InsideFormattedValue::Inside(SurroundingFStringQuotes { closest, .. }) => { + closest + } + InsideFormattedValue::Outside => unreachable!(), + }; + + let string_content = f.context().locator().slice(*range); + let (normalized, _contains_newlines) = + normalize_string(string_content, preferred_quotes, false); + write!(f, [dynamic_text(&normalized, None)]) + } + FStringPart::FormattedValue(formatted_value) => { + write!(f, [formatted_value.format()]) + } + } + } +} diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index e7eb28ae7f4fd..1090687f41877 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -5,10 +5,13 @@ pub(crate) mod comprehension; pub(crate) mod decorator; pub(crate) mod elif_else_clause; pub(crate) mod except_handler_except_handler; +pub(crate) mod formatted_value; +pub(crate) mod fstring_part; pub(crate) mod identifier; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod parameter; pub(crate) mod parameter_with_default; pub(crate) mod parameters; +pub(crate) mod partial_string; pub(crate) mod with_item; diff --git a/crates/ruff_python_formatter/src/other/partial_string.rs b/crates/ruff_python_formatter/src/other/partial_string.rs new file mode 100644 index 0000000000000..9605d93d0d5e0 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/partial_string.rs @@ -0,0 +1,12 @@ +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::FormatResult; +use ruff_python_ast::PartialString; + +#[derive(Default)] +pub struct FormatPartialString; + +impl FormatNodeRule for FormatPartialString { + fn fmt_fields(&self, _item: &PartialString, _f: &mut PyFormatter) -> FormatResult<()> { + unreachable!("Handled inside of `FormatExprFString"); + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap index 022b30a5a30a9..124aa318d1386 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap @@ -83,6 +83,13 @@ f"\"{a}\"{'hello' * b}\"{c}\"" re.compile(r'[\\"]') "x = ''; y = \"\"" "x = '''; y = \"\"" +@@ -48,5 +53,5 @@ + + # We must bail out if changing the quotes would introduce backslashes in f-string + # expressions. xref: https://github.com/psf/black/issues/2348 +-f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" ++f"\"{b}\"{' ' * (long - len(b) + 1)}: \"{sts}\",\n" + f"\"{a}\"{'hello' * b}\"{c}\"" ``` ## Ruff Output @@ -143,7 +150,7 @@ f"{y * x} '{z}'" # We must bail out if changing the quotes would introduce backslashes in f-string # expressions. xref: https://github.com/psf/black/issues/2348 -f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{b}\"{' ' * (long - len(b) + 1)}: \"{sts}\",\n" f"\"{a}\"{'hello' * b}\"{c}\"" ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572_remove_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572_remove_parens.py.snap index daea36ba7ec65..fdd3d8ad5002c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572_remove_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572_remove_parens.py.snap @@ -81,7 +81,7 @@ async def await_the_walrus(): ```diff --- Black +++ Ruff -@@ -34,7 +34,7 @@ +@@ -34,10 +34,10 @@ lambda: (x := 1) a[(x := 12)] @@ -89,7 +89,11 @@ async def await_the_walrus(): +a[: (x := 13)] # we don't touch expressions in f-strings but if we do one day, don't break 'em - f"{(x:=10)}" +-f"{(x:=10)}" ++f"{(x := 10)}" + + + def a(): ``` ## Ruff Output @@ -134,7 +138,7 @@ a[(x := 12)] a[: (x := 13)] # we don't touch expressions in f-strings but if we do one day, don't break 'em -f"{(x:=10)}" +f"{(x := 10)}" def a(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap new file mode 100644 index 0000000000000..c227afb147c54 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap @@ -0,0 +1,69 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py +--- +## Input + +```py +f"f-string without formatted values is just a string" +f"{{NOT a formatted value}}" +f"{{NOT 'a' \"formatted\" \"value\"}}" +f"some f-string with {a} {few():.2f} {formatted.values!r}" +f'some f-string with {a} {few(""):.2f} {formatted.values!r}' +f"{f'''{'nested'} inner'''} outer" +f"\"{f'{nested} inner'}\" outer" +f"space between opening braces: { {a for a in (1, 2, 3)}}" +f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -5,7 +5,7 @@ + f'some f-string with {a} {few(""):.2f} {formatted.values!r}' + f"{f'''{'nested'} inner'''} outer" + f"\"{f'{nested} inner'}\" outer" +-f"space between opening braces: { {a for a in (1, 2, 3)}}" ++f"space between opening braces: { {a for a in (1, 2, 3)} }" + f'Hello \'{tricky + "example"}\'' + f"Tried directories {str(rootdirs)} \ + but none started with prefix {parentdir_prefix}" +``` + +## Ruff Output + +```py +f"f-string without formatted values is just a string" +f"{{NOT a formatted value}}" +f'{{NOT \'a\' "formatted" "value"}}' +f"some f-string with {a} {few():.2f} {formatted.values!r}" +f'some f-string with {a} {few(""):.2f} {formatted.values!r}' +f"{f'''{'nested'} inner'''} outer" +f"\"{f'{nested} inner'}\" outer" +f"space between opening braces: { {a for a in (1, 2, 3)} }" +f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" +``` + +## Black Output + +```py +f"f-string without formatted values is just a string" +f"{{NOT a formatted value}}" +f'{{NOT \'a\' "formatted" "value"}}' +f"some f-string with {a} {few():.2f} {formatted.values!r}" +f'some f-string with {a} {few(""):.2f} {formatted.values!r}' +f"{f'''{'nested'} inner'''} outer" +f"\"{f'{nested} inner'}\" outer" +f"space between opening braces: { {a for a in (1, 2, 3)}}" +f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index aa3f0a5133aae..2b5e9d4c43d8c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -9,7 +9,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression f'{two}' ) - +# quote handling for raw f-strings rf"Not-so-tricky \"quote" # Regression test for fstrings dropping comments @@ -39,25 +39,83 @@ result_f = ( # comment '' ) + + +f"{ chr(65) = }" +f"{ chr(65) = !s}" +f"{ chr(65) = !r}" +f"{ chr(65) = :#x}" +f"{a=!r:0.05f}" + +f"{ {} = }" +f"{ {}=}" + +# should add some nice spaces +f"{1-2+3}" + + +# don't switch quotes inside a formatted value +f"\"{f'{nested} inner'}\" outer" + +# need a space to avoid escaping the curly +f"{ {1}}" + +# extra spaces don't interfere with debug_text +f"{ {1}=}" + +# handle string continuations +( + '' + f'"{1}' +) + +# it's ok to change triple quotes with even with qoutes inside f-strings +f''' {""} ''' + +# it's ok to change the inner single to a double quote +f""" {f' {1}'} """ +f''' {f' {1}'} ''' + +# TODO: would we want to swap the quotes to use double on the outside? +f' {f" {1}"} ' +f''' {f""" {1}"""} ''' + +# various nested quotes to leave unchanged +f" {f' {1}'} " +f""" {f" {1}"} """ +f""" {f''' {1}'''} """ +f"{f'''{'nested'} inner'''} outer" + + +# must not break lines inside non-triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" + +) +# TODO: ok to ot break lines inside triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" + +) ``` ## Output ```py (f"{one}" f"{two}") - +# quote handling for raw f-strings rf"Not-so-tricky \"quote" # Regression test for fstrings dropping comments result_f = ( "Traceback (most recent call last):\n" - f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + f' File "{__file__}", line {lineno_f + 5}, in _check_recursive_traceback_display\n' " f()\n" - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f + 1}, in f\n' " f()\n" - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f + 1}, in f\n' " f()\n" - f' File "{__file__}", line {lineno_f+1}, in f\n' + f' File "{__file__}", line {lineno_f + 1}, in f\n' " f()\n" # XXX: The following line changes depending on whether the tests # are run through the interactive interpreter or with -m @@ -76,6 +134,59 @@ result_f = ( # comment "" ) + + +f"{ chr(65) = }" +f"{ chr(65) = !s}" +f"{ chr(65) = !r}" +f"{ chr(65) = :#x}" +f"{a=!r:0.05f}" + +f"{ {} = }" +f"{ {}=}" + +# should add some nice spaces +f"{1 - 2 + 3}" + + +# don't switch quotes inside a formatted value +f"\"{f'{nested} inner'}\" outer" + +# need a space to avoid escaping the curly +f"{ {1} }" + +# extra spaces don't interfere with debug_text +f"{ {1}=}" + +# handle string continuations +("" f'"{1}') + +# it's ok to change triple quotes with even with qoutes inside f-strings +f""" {""} """ + +# it's ok to change the inner single to a double quote +f""" {f" {1}"} """ +f""" {f" {1}"} """ + +# TODO: would we want to swap the quotes to use double on the outside? +f' {f" {1}"} ' +f''' {f""" {1}"""} ''' + +# various nested quotes to leave unchanged +f" {f' {1}'} " +f""" {f" {1}"} """ +f""" {f''' {1}'''} """ +f"{f'''{'nested'} inner'''} outer" + + +# must not break lines inside non-triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" +) +# TODO: ok to ot break lines inside triple quoted f-strings +print( + f"Finished {f'{1}'} updating analytics counts through {fill_to_time} in {time.time() - start:.3f}s" +) ``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__parse_f_string.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__parse_f_string.snap index f65e153bea1a3..f80a93face317 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__parse_f_string.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__parse_f_string.snap @@ -9,21 +9,15 @@ expression: parse_ast value: FString( ExprFString { range: 0..14, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 2..13, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: false, - }, - ), - kind: None, + value: "Hello world", }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try.snap index e3fd432492174..f7b16d138db73 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try.snap @@ -82,23 +82,18 @@ expression: parse_ast FString( ExprFString { range: 62..81, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 64..71, - value: Str( - StringConstant { - value: "caught ", - implicit_concatenated: false, - }, - ), - kind: None, + value: "caught ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 71..80, - value: Call( + expression: Call( ExprCall { range: 72..79, func: Name( @@ -125,11 +120,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), ], @@ -180,23 +174,18 @@ expression: parse_ast FString( ExprFString { range: 114..133, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 116..123, - value: Str( - StringConstant { - value: "caught ", - implicit_concatenated: false, - }, - ), - kind: None, + value: "caught ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 123..132, - value: Call( + expression: Call( ExprCall { range: 124..131, func: Name( @@ -223,11 +212,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), ], diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try_star.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try_star.snap index 7940ea7359489..9bfc37f5adb8d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try_star.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__parser__tests__try_star.snap @@ -201,23 +201,18 @@ expression: parse_ast FString( ExprFString { range: 133..179, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 135..142, - value: Str( - StringConstant { - value: "caught ", - implicit_concatenated: false, - }, - ), - kind: None, + value: "caught ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 142..151, - value: Call( + expression: Call( ExprCall { range: 143..150, func: Name( @@ -244,25 +239,19 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), - Constant( - ExprConstant { + Literal( + PartialString { range: 151..164, - value: Str( - StringConstant { - value: " with nested ", - implicit_concatenated: false, - }, - ), - kind: None, + value: " with nested ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 164..178, - value: Attribute( + expression: Attribute( ExprAttribute { range: 165..177, value: Name( @@ -281,11 +270,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), ], @@ -336,23 +324,18 @@ expression: parse_ast FString( ExprFString { range: 213..259, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 215..222, - value: Str( - StringConstant { - value: "caught ", - implicit_concatenated: false, - }, - ), - kind: None, + value: "caught ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 222..231, - value: Call( + expression: Call( ExprCall { range: 223..230, func: Name( @@ -379,25 +362,19 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), - Constant( - ExprConstant { + Literal( + PartialString { range: 231..244, - value: Str( - StringConstant { - value: " with nested ", - implicit_concatenated: false, - }, - ), - kind: None, + value: " with nested ", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 244..258, - value: Attribute( + expression: Attribute( ExprAttribute { range: 245..257, value: Name( @@ -416,11 +393,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), ], diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap index 6b4a1a92c819b..2b55b4e275eee 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..22, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 2..5, - value: Str( - StringConstant { - value: "aaa", - implicit_concatenated: false, - }, - ), - kind: None, + value: "aaa", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 5..10, - value: Name( + expression: Name( ExprName { range: 6..9, id: "bbb", @@ -34,25 +29,19 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), - Constant( - ExprConstant { + Literal( + PartialString { range: 10..13, - value: Str( - StringConstant { - value: "ccc", - implicit_concatenated: false, - }, - ), - kind: None, + value: "ccc", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 13..18, - value: Name( + expression: Name( ExprName { range: 14..17, id: "ddd", @@ -61,23 +50,16 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), - Constant( - ExprConstant { + Literal( + PartialString { range: 18..21, - value: Str( - StringConstant { - value: "eee", - implicit_concatenated: false, - }, - ), - kind: None, + value: "eee", }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap index d96a1cef58695..9adcbf6008b5f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..8, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 2..4, - value: Str( - StringConstant { - value: "\\", - implicit_concatenated: false, - }, - ), - kind: None, + value: "\\", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 4..7, - value: Name( + expression: Name( ExprName { range: 5..6, id: "x", @@ -34,11 +29,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap index fe3c6d028e39c..9800251950494 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..8, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 2..4, - value: Str( - StringConstant { - value: "\n", - implicit_concatenated: false, - }, - ), - kind: None, + value: "\n", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 4..7, - value: Name( + expression: Name( ExprName { range: 5..6, id: "x", @@ -34,11 +29,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap index 35cff59a24ab1..a8b64fb64af0c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..9, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 3..5, - value: Str( - StringConstant { - value: "\\\n", - implicit_concatenated: false, - }, - ), - kind: None, + value: "\\\n", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 5..8, - value: Name( + expression: Name( ExprName { range: 6..7, id: "x", @@ -34,11 +29,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap index 2ea0c26e91d2d..bb257fdc4349b 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap @@ -2,25 +2,31 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..9, - value: Name( - ExprName { - range: 3..7, - id: "user", - ctx: Load, +FString( + ExprFString { + range: 2..9, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..9, + expression: Name( + ExprName { + range: 3..7, + id: "user", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: [], }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: "=", - }, - ), - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap index 655b3cc3d8af5..5d2f649895b45 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap @@ -2,69 +2,63 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - Constant( - ExprConstant { - range: 2..6, - value: Str( - StringConstant { +FString( + ExprFString { + range: 2..37, + implicit_concatenated: false, + parts: [ + Literal( + PartialString { + range: 2..6, value: "mix ", - implicit_concatenated: false, }, ), - kind: None, - }, - ), - FormattedValue( - ExprFormattedValue { - range: 6..13, - value: Name( - ExprName { - range: 7..11, - id: "user", - ctx: Load, + FormattedValue( + FormattedValue { + range: 6..13, + expression: Name( + ExprName { + range: 7..11, + id: "user", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: [], }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: "=", - }, - ), - conversion: None, - format_spec: None, - }, - ), - Constant( - ExprConstant { - range: 13..28, - value: Str( - StringConstant { + Literal( + PartialString { + range: 13..28, value: " with text and ", - implicit_concatenated: false, - }, - ), - kind: None, - }, - ), - FormattedValue( - ExprFormattedValue { - range: 28..37, - value: Name( - ExprName { - range: 29..35, - id: "second", - ctx: Load, }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: "=", + FormattedValue( + FormattedValue { + range: 28..37, + expression: Name( + ExprName { + range: 29..35, + id: "second", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: [], }, ), - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap index 22b250a8ce768..0a6dad037ecd8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap @@ -2,46 +2,38 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..13, - value: Name( - ExprName { - range: 3..7, - id: "user", - ctx: Load, +FString( + ExprFString { + range: 2..13, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..13, + expression: Name( + ExprName { + range: 3..7, + id: "user", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: [ + Literal( + PartialString { + range: 9..12, + value: ">10", + }, + ), + ], }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: "=", - }, - ), - conversion: None, - format_spec: Some( - FString( - ExprFString { - range: 9..12, - values: [ - Constant( - ExprConstant { - range: 9..12, - value: Str( - StringConstant { - value: ">10", - implicit_concatenated: false, - }, - ), - kind: None, - }, - ), - ], - implicit_concatenated: false, - }, - ), - ), - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap index 0b711c8cc91a3..f4dfd26711c8a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..11, - values: [ - Constant( - ExprConstant { + implicit_concatenated: false, + parts: [ + Literal( + PartialString { range: 4..5, - value: Str( - StringConstant { - value: "\n", - implicit_concatenated: false, - }, - ), - kind: None, + value: "\n", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 5..8, - value: Name( + expression: Name( ExprName { range: 6..7, id: "x", @@ -34,11 +29,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap index a1435da1835d3..c704373b44aed 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap @@ -2,4 +2,10 @@ source: crates/ruff_python_parser/src/string.rs expression: "parse_fstring(\"\").unwrap()" --- -[] +FString( + ExprFString { + range: 2..2, + implicit_concatenated: false, + parts: [], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap index 8680272f60834..3d4fb7e40b1b3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap @@ -9,21 +9,15 @@ expression: parse_ast value: FString( ExprFString { range: 0..17, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 1..16, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: true, - }, - ), - kind: None, + value: "Hello world", }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap index 8680272f60834..3d4fb7e40b1b3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap @@ -9,21 +9,15 @@ expression: parse_ast value: FString( ExprFString { range: 0..17, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 1..16, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: true, - }, - ), - kind: None, + value: "Hello world", }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap index 2c5e80aad5eff..0ba2643ce5e68 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..22, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 1..16, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: true, - }, - ), - kind: None, + value: "Hello world", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 16..21, - value: Constant( + expression: Constant( ExprConstant { range: 17..20, value: Str( @@ -39,11 +34,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap index 19e8b7ae045d7..b3bfcadf981b8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap @@ -9,23 +9,18 @@ expression: parse_ast value: FString( ExprFString { range: 0..31, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 1..16, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: true, - }, - ), - kind: None, + value: "Hello world", }, ), FormattedValue( - ExprFormattedValue { + FormattedValue { range: 16..21, - value: Constant( + expression: Constant( ExprConstant { range: 17..20, value: Str( @@ -39,23 +34,16 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), - Constant( - ExprConstant { + Literal( + PartialString { range: 24..30, - value: Str( - StringConstant { - value: "again!", - implicit_concatenated: true, - }, - ), - kind: None, + value: "again!", }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap index 282eb79f97d88..9e387b6f4f4fc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap @@ -2,47 +2,47 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..5, - value: Name( - ExprName { - range: 3..4, - id: "a", - ctx: Load, +FString( + ExprFString { + range: 2..17, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..5, + expression: Name( + ExprName { + range: 3..4, + id: "a", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], }, ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), - FormattedValue( - ExprFormattedValue { - range: 5..10, - value: Name( - ExprName { - range: 7..8, - id: "b", - ctx: Load, + FormattedValue( + FormattedValue { + range: 5..10, + expression: Name( + ExprName { + range: 7..8, + id: "b", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], }, ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), - Constant( - ExprConstant { - range: 10..17, - value: Str( - StringConstant { + Literal( + PartialString { + range: 10..17, value: "{foo}", - implicit_concatenated: false, }, ), - kind: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap index 5fc8c4d26421b..28ba40efd5ad8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap @@ -2,41 +2,47 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..12, - value: Compare( - ExprCompare { - range: 3..11, - left: Constant( - ExprConstant { - range: 3..5, - value: Int( - 42, +FString( + ExprFString { + range: 2..12, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..12, + expression: Compare( + ExprCompare { + range: 3..11, + left: Constant( + ExprConstant { + range: 3..5, + value: Int( + 42, + ), + kind: None, + }, ), - kind: None, + ops: [ + Eq, + ], + comparators: [ + Constant( + ExprConstant { + range: 9..11, + value: Int( + 42, + ), + kind: None, + }, + ), + ], }, ), - ops: [ - Eq, - ], - comparators: [ - Constant( - ExprConstant { - range: 9..11, - value: Int( - 42, - ), - kind: None, - }, - ), - ], + debug_text: None, + conversion: None, + format_spec: [], }, ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap index 97525a5e10db8..5b2c997fb73bc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap @@ -2,49 +2,47 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..15, - value: Name( - ExprName { - range: 3..6, - id: "foo", - ctx: Load, +FString( + ExprFString { + range: 2..15, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..15, + expression: Name( + ExprName { + range: 3..6, + id: "foo", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [ + FormattedValue( + FormattedValue { + range: 7..14, + expression: Constant( + ExprConstant { + range: 8..13, + value: Str( + StringConstant { + value: "", + implicit_concatenated: true, + }, + ), + kind: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], + }, + ), + ], }, ), - debug_text: None, - conversion: None, - format_spec: Some( - FString( - ExprFString { - range: 7..14, - values: [ - FormattedValue( - ExprFormattedValue { - range: 7..14, - value: Constant( - ExprConstant { - range: 8..13, - value: Str( - StringConstant { - value: "", - implicit_concatenated: true, - }, - ), - kind: None, - }, - ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), - ], - implicit_concatenated: false, - }, - ), - ), - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap index d93be4602f04e..9fd8c07f5abce 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap @@ -2,44 +2,42 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..14, - value: Name( - ExprName { - range: 3..6, - id: "foo", - ctx: Load, +FString( + ExprFString { + range: 2..14, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..14, + expression: Name( + ExprName { + range: 3..6, + id: "foo", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [ + FormattedValue( + FormattedValue { + range: 7..13, + expression: Name( + ExprName { + range: 8..12, + id: "spec", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], + }, + ), + ], }, ), - debug_text: None, - conversion: None, - format_spec: Some( - FString( - ExprFString { - range: 7..13, - values: [ - FormattedValue( - ExprFormattedValue { - range: 7..13, - value: Name( - ExprName { - range: 8..12, - id: "spec", - ctx: Load, - }, - ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), - ], - implicit_concatenated: false, - }, - ), - ), - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap index 31db5e6cf8997..a84f8d16cfb7b 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap @@ -2,49 +2,47 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..12, - value: Name( - ExprName { - range: 3..6, - id: "foo", - ctx: Load, +FString( + ExprFString { + range: 2..12, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: "foo", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [ + FormattedValue( + FormattedValue { + range: 7..11, + expression: Constant( + ExprConstant { + range: 8..10, + value: Str( + StringConstant { + value: "", + implicit_concatenated: false, + }, + ), + kind: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], + }, + ), + ], }, ), - debug_text: None, - conversion: None, - format_spec: Some( - FString( - ExprFString { - range: 7..11, - values: [ - FormattedValue( - ExprFormattedValue { - range: 7..11, - value: Constant( - ExprConstant { - range: 8..10, - value: Str( - StringConstant { - value: "", - implicit_concatenated: false, - }, - ), - kind: None, - }, - ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), - ], - implicit_concatenated: false, - }, - ), - ), - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap index 070ea1f20b27f..2b4d43cd72141 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap @@ -2,41 +2,47 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..10, - value: Compare( - ExprCompare { - range: 3..9, - left: Constant( - ExprConstant { - range: 3..4, - value: Int( - 1, +FString( + ExprFString { + range: 2..10, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..10, + expression: Compare( + ExprCompare { + range: 3..9, + left: Constant( + ExprConstant { + range: 3..4, + value: Int( + 1, + ), + kind: None, + }, ), - kind: None, + ops: [ + NotEq, + ], + comparators: [ + Constant( + ExprConstant { + range: 8..9, + value: Int( + 2, + ), + kind: None, + }, + ), + ], }, ), - ops: [ - NotEq, - ], - comparators: [ - Constant( - ExprConstant { - range: 8..9, - value: Int( - 2, - ), - kind: None, - }, - ), - ], + debug_text: None, + conversion: None, + format_spec: [], }, ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap index 5a621fc857611..572ca3349f858 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap @@ -2,41 +2,33 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..12, - value: Name( - ExprName { - range: 3..6, - id: "foo", - ctx: Load, +FString( + ExprFString { + range: 2..12, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: "foo", + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: [ + Literal( + PartialString { + range: 7..11, + value: "spec", + }, + ), + ], }, ), - debug_text: None, - conversion: None, - format_spec: Some( - FString( - ExprFString { - range: 7..11, - values: [ - Constant( - ExprConstant { - range: 7..11, - value: Str( - StringConstant { - value: "spec", - implicit_concatenated: false, - }, - ), - kind: None, - }, - ), - ], - implicit_concatenated: false, - }, - ), - ), - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap index 1891d37330dd0..d74d5dc69b35d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap @@ -2,25 +2,31 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..9, - value: Name( - ExprName { - range: 3..4, - id: "x", - ctx: Load, +FString( + ExprFString { + range: 2..9, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: "x", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: [], }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: " =", - }, - ), - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap index aa84db5f3f4fa..721aea1ca9ff2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap @@ -2,25 +2,31 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..9, - value: Name( - ExprName { - range: 3..4, - id: "x", - ctx: Load, +FString( + ExprFString { + range: 2..9, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: "x", + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "= ", + }, + ), + conversion: None, + format_spec: [], }, ), - debug_text: Some( - DebugText { - leading: "", - trailing: "= ", - }, - ), - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap index d52ab525d43db..a9f2af8c6e21c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap @@ -2,19 +2,25 @@ source: crates/ruff_python_parser/src/string.rs expression: parse_ast --- -[ - FormattedValue( - ExprFormattedValue { - range: 2..9, - value: Yield( - ExprYield { - range: 3..8, - value: None, +FString( + ExprFString { + range: 2..9, + implicit_concatenated: false, + parts: [ + FormattedValue( + FormattedValue { + range: 2..9, + expression: Yield( + ExprYield { + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: [], }, ), - debug_text: None, - conversion: None, - format_spec: None, - }, - ), -] + ], + }, +) diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap index 23593fee07d09..564dfb67bd0b2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap @@ -9,23 +9,15 @@ expression: parse_ast value: FString( ExprFString { range: 0..18, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 2..17, - value: Str( - StringConstant { - value: "Hello world", - implicit_concatenated: true, - }, - ), - kind: Some( - "u", - ), + value: "Hello world", }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap index e6a8a74995440..c9b8a9b34007d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap @@ -9,23 +9,15 @@ expression: parse_ast value: FString( ExprFString { range: 0..22, - values: [ - Constant( - ExprConstant { + implicit_concatenated: true, + parts: [ + Literal( + PartialString { range: 2..21, - value: Str( - StringConstant { - value: "Hello world!", - implicit_concatenated: true, - }, - ), - kind: Some( - "u", - ), + value: "Hello world!", }, ), ], - implicit_concatenated: true, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap index 65f4daf83d8d7..e8b37db3235b0 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap @@ -9,11 +9,12 @@ expression: parse_ast value: FString( ExprFString { range: 0..7, - values: [ + implicit_concatenated: false, + parts: [ FormattedValue( - ExprFormattedValue { + FormattedValue { range: 3..6, - value: Name( + expression: Name( ExprName { range: 4..5, id: "x", @@ -22,11 +23,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap index 6793e65f73804..f81548a8a85d8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap @@ -9,11 +9,12 @@ expression: parse_ast value: FString( ExprFString { range: 0..11, - values: [ + implicit_concatenated: false, + parts: [ FormattedValue( - ExprFormattedValue { + FormattedValue { range: 5..8, - value: Name( + expression: Name( ExprName { range: 6..7, id: "x", @@ -22,11 +23,10 @@ expression: parse_ast ), debug_text: None, conversion: None, - format_spec: None, + format_spec: [], }, ), ], - implicit_concatenated: false, }, ), }, diff --git a/crates/ruff_python_parser/src/string.rs b/crates/ruff_python_parser/src/string.rs index beeb5b9dc0942..af69d5653bf22 100644 --- a/crates/ruff_python_parser/src/string.rs +++ b/crates/ruff_python_parser/src/string.rs @@ -173,19 +173,23 @@ impl<'a> StringParser<'a> { } } - fn parse_formatted_value(&mut self, nested: u8) -> Result, LexicalError> { + fn parse_formatted_value(&mut self, nested: u8) -> Result, LexicalError> { use FStringErrorType::{ - EmptyExpression, InvalidConversionFlag, InvalidExpression, MismatchedDelimiter, - UnclosedLbrace, Unmatched, UnterminatedString, + EmptyExpression, ExpressionNestedTooDeeply, InvalidConversionFlag, InvalidExpression, + MismatchedDelimiter, UnclosedLbrace, Unmatched, UnterminatedString, }; + if nested >= 2 { + return Err(FStringError::new(ExpressionNestedTooDeeply, self.get_pos()).into()); + } + let mut expression = String::new(); // for self-documenting strings we also store the `=` and any trailing space inside // expression (because we want to combine it with any trailing spaces before the equal // sign). the expression_length is the length of the actual expression part that we pass to // `parse_fstring_expr` let mut expression_length = 0; - let mut spec = None; + let mut spec = vec![]; let mut delimiters = Vec::new(); let mut conversion = ConversionFlag::None; let mut self_documenting = false; @@ -238,14 +242,7 @@ impl<'a> StringParser<'a> { } ':' if delimiters.is_empty() => { - let start_location = self.get_pos(); - let parsed_spec = self.parse_spec(nested)?; - - spec = Some(Box::new(Expr::from(ast::ExprFString { - values: parsed_spec, - implicit_concatenated: false, - range: self.range(start_location), - }))); + spec = self.parse_spec(nested)?; } '(' | '{' | '[' => { expression.push(ch); @@ -320,8 +317,8 @@ impl<'a> StringParser<'a> { let leading = &expression[..usize::from(value.start() - start_location) - 1]; let trailing = &expression[usize::from(value.end() - start_location) - 1..]; - vec![Expr::from(ast::ExprFormattedValue { - value: Box::new(value), + vec![ast::FStringPart::FormattedValue(ast::FormattedValue { + expression: Box::new(value), debug_text: Some(ast::DebugText { leading: leading.to_string(), trailing: trailing.to_string(), @@ -331,8 +328,8 @@ impl<'a> StringParser<'a> { range: self.range(start_location), })] } else { - vec![Expr::from(ast::ExprFormattedValue { - value: Box::new( + vec![ast::FStringPart::FormattedValue(ast::FormattedValue { + expression: Box::new( parse_fstring_expr(&expression, start_location).map_err(|e| { FStringError::new( InvalidExpression(Box::new(e.error)), @@ -376,7 +373,7 @@ impl<'a> StringParser<'a> { Err(FStringError::new(UnclosedLbrace, self.get_pos()).into()) } - fn parse_spec(&mut self, nested: u8) -> Result, LexicalError> { + fn parse_spec(&mut self, nested: u8) -> Result, LexicalError> { let mut spec_constructor = Vec::new(); let mut constant_piece = String::new(); let mut start_location = self.get_pos(); @@ -384,13 +381,12 @@ impl<'a> StringParser<'a> { match next { '{' => { if !constant_piece.is_empty() { - spec_constructor.push(Expr::from(ast::ExprConstant { - value: std::mem::take(&mut constant_piece).into(), - kind: None, + spec_constructor.push(ast::FStringPart::Literal(ast::PartialString { + value: std::mem::take(&mut constant_piece), range: self.range(start_location), })); } - let parsed_expr = self.parse_fstring(nested + 1)?; + let parsed_expr = self.parse_formatted_value(nested + 1)?; spec_constructor.extend(parsed_expr); start_location = self.get_pos(); continue; @@ -405,25 +401,21 @@ impl<'a> StringParser<'a> { self.next_char(); } if !constant_piece.is_empty() { - spec_constructor.push(Expr::from(ast::ExprConstant { - value: std::mem::take(&mut constant_piece).into(), - kind: None, + spec_constructor.push(ast::FStringPart::Literal(ast::PartialString { + value: std::mem::take(&mut constant_piece), range: self.range(start_location), })); } Ok(spec_constructor) } - fn parse_fstring(&mut self, nested: u8) -> Result, LexicalError> { - use FStringErrorType::{ExpressionNestedTooDeeply, SingleRbrace, UnclosedLbrace}; - - if nested >= 2 { - return Err(FStringError::new(ExpressionNestedTooDeeply, self.get_pos()).into()); - } + fn parse_fstring(&mut self, nested: u8) -> Result { + use FStringErrorType::{SingleRbrace, UnclosedLbrace}; let mut content = String::new(); - let mut start_location = self.get_pos(); - let mut values = vec![]; + let start_location = self.get_pos(); + let mut part_start_location = self.get_pos(); + let mut parts = vec![]; while let Some(ch) = self.peek() { match ch { @@ -443,16 +435,15 @@ impl<'a> StringParser<'a> { } } if !content.is_empty() { - values.push(Expr::from(ast::ExprConstant { - value: std::mem::take(&mut content).into(), - kind: None, - range: self.range(start_location), + parts.push(ast::FStringPart::Literal(ast::PartialString { + value: std::mem::take(&mut content), + range: self.range(part_start_location), })); } - let parsed_values = self.parse_formatted_value(nested)?; - values.extend(parsed_values); - start_location = self.get_pos(); + let parsed_parts = self.parse_formatted_value(nested)?; + parts.extend(parsed_parts); + part_start_location = self.get_pos(); } '}' => { if nested > 0 { @@ -478,14 +469,17 @@ impl<'a> StringParser<'a> { } if !content.is_empty() { - values.push(Expr::from(ast::ExprConstant { - value: content.into(), - kind: None, - range: self.range(start_location), + parts.push(ast::FStringPart::Literal(ast::PartialString { + value: content, + range: self.range(part_start_location), })); } - Ok(values) + Ok(Expr::from(ast::ExprFString { + parts, + implicit_concatenated: false, + range: self.range(start_location), + })) } fn parse_bytes(&mut self) -> Result { @@ -535,13 +529,13 @@ impl<'a> StringParser<'a> { })) } - fn parse(&mut self) -> Result, LexicalError> { + fn parse(&mut self) -> Result { if self.kind.is_any_fstring() { self.parse_fstring(0) } else if self.kind.is_any_bytes() { - self.parse_bytes().map(|expr| vec![expr]) + self.parse_bytes() } else { - self.parse_string().map(|expr| vec![expr]) + self.parse_string() } } } @@ -556,7 +550,7 @@ fn parse_string( kind: StringKind, triple_quoted: bool, start: TextSize, -) -> Result, LexicalError> { +) -> Result { StringParser::new(source, kind, triple_quoted, start).parse() } @@ -589,14 +583,13 @@ pub(crate) fn parse_strings( if has_bytes { let mut content: Vec = vec![]; for (start, (source, kind, triple_quoted), _) in values { - for value in parse_string(&source, kind, triple_quoted, start)? { - match value { - Expr::Constant(ast::ExprConstant { - value: Constant::Bytes(BytesConstant { value, .. }), - .. - }) => content.extend(value), - _ => unreachable!("Unexpected non-bytes expression."), - } + let value = parse_string(&source, kind, triple_quoted, start)?; + match value { + Expr::Constant(ast::ExprConstant { + value: Constant::Bytes(BytesConstant { value, .. }), + .. + }) => content.extend(value), + _ => unreachable!("Unexpected non-bytes expression."), } } return Ok(ast::ExprConstant { @@ -613,14 +606,13 @@ pub(crate) fn parse_strings( if !has_fstring { let mut content: Vec = vec![]; for (start, (source, kind, triple_quoted), _) in values { - for value in parse_string(&source, kind, triple_quoted, start)? { - match value { - Expr::Constant(ast::ExprConstant { - value: Constant::Str(StringConstant { value, .. }), - .. - }) => content.push(value), - _ => unreachable!("Unexpected non-string expression."), - } + let value = parse_string(&source, kind, triple_quoted, start)?; + match value { + Expr::Constant(ast::ExprConstant { + value: Constant::Str(StringConstant { value, .. }), + .. + }) => content.push(value), + _ => unreachable!("Unexpected non-string expression."), } } return Ok(ast::ExprConstant { @@ -635,44 +627,59 @@ pub(crate) fn parse_strings( } // De-duplicate adjacent constants. - let mut deduped: Vec = vec![]; + let mut deduped: Vec = vec![]; let mut current: Vec = vec![]; let mut current_start = initial_start; let mut current_end = last_end; - let take_current = |current: &mut Vec, start, end| -> Expr { - Expr::Constant(ast::ExprConstant { - value: Constant::Str(StringConstant { - value: current.drain(..).collect::(), - implicit_concatenated, - }), - kind: initial_kind.clone(), + let take_current = |current: &mut Vec, start, end| -> ast::FStringPart { + ast::FStringPart::Literal(ast::PartialString { + value: current.drain(..).collect::(), range: TextRange::new(start, end), }) }; for (start, (source, kind, triple_quoted), _) in values { - for value in parse_string(&source, kind, triple_quoted, start)? { - let value_range = value.range(); - match value { - Expr::FormattedValue { .. } => { - if !current.is_empty() { - deduped.push(take_current(&mut current, current_start, current_end)); + let value = parse_string(&source, kind, triple_quoted, start)?; + match value { + Expr::FString(ast::ExprFString { parts, .. }) => { + for part in parts { + match part { + ast::FStringPart::Literal(ast::PartialString { + value: inner, + range, + }) => { + if current.is_empty() { + current_start = range.start(); + } + current_end = range.end(); + current.push(inner); + } + ast::FStringPart::FormattedValue(ast::FormattedValue { .. }) => { + if !current.is_empty() { + deduped.push(take_current( + &mut current, + current_start, + current_end, + )); + } + deduped.push(part); + } } - deduped.push(value); } - Expr::Constant(ast::ExprConstant { - value: Constant::Str(StringConstant { value, .. }), - .. - }) => { - if current.is_empty() { - current_start = value_range.start(); - } - current_end = value_range.end(); - current.push(value); + } + Expr::Constant(ast::ExprConstant { + value: Constant::Str(StringConstant { value, .. }), + range, + kind: _, + }) => { + if current.is_empty() { + current_start = range.start(); } - _ => unreachable!("Unexpected non-string expression."), + current_end = range.end(); + current.push(value); } + expr => unreachable!("Unexpected non-string expression: `{expr:?}`."), } } if !current.is_empty() { @@ -680,7 +687,7 @@ pub(crate) fn parse_strings( } Ok(Expr::FString(ast::ExprFString { - values: deduped, + parts: deduped, implicit_concatenated, range: TextRange::new(initial_start, last_end), })) @@ -797,7 +804,7 @@ mod tests { use super::*; use crate::parser::parse_suite; - fn parse_fstring(source: &str) -> Result, LexicalError> { + fn parse_fstring(source: &str) -> Result { StringParser::new(source, StringKind::FString, false, TextSize::default()).parse() } diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index 4bba5b9826010..890e2a8e952e1 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -302,7 +302,6 @@ impl From<&Expr> for ResolvedPythonType { | Expr::YieldFrom(_) | Expr::Compare(_) | Expr::Call(_) - | Expr::FormattedValue(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Starred(_)