diff --git a/crates/ruff_python_ast/src/expression.rs b/crates/ruff_python_ast/src/expression.rs index b3bd0a6701..a4a9c044ba 100644 --- a/crates/ruff_python_ast/src/expression.rs +++ b/crates/ruff_python_ast/src/expression.rs @@ -2,7 +2,10 @@ use std::iter::FusedIterator; use ruff_text_size::{Ranged, TextRange}; -use crate::{self as ast, AnyNodeRef, AnyStringFlags, Expr}; +use crate::{ + self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString, + ExprStringLiteral, StringFlags, +}; /// Unowned pendant to [`ast::Expr`] that stores a reference instead of a owned value. #[derive(Copy, Clone, Debug, PartialEq)] @@ -405,6 +408,10 @@ pub enum StringLike<'a> { } impl<'a> StringLike<'a> { + pub const fn is_fstring(self) -> bool { + matches!(self, Self::FString(_)) + } + /// Returns an iterator over the [`StringLikePart`] contained in this string-like expression. pub fn parts(&self) -> StringLikePartIter<'_> { match self { @@ -413,6 +420,15 @@ impl<'a> StringLike<'a> { StringLike::FString(expr) => StringLikePartIter::FString(expr.value.iter()), } } + + /// Returns `true` if the string is implicitly concatenated. + pub fn is_implicit_concatenated(self) -> bool { + match self { + Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(), + Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(), + Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(), + } + } } impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> { @@ -433,6 +449,45 @@ impl<'a> From<&'a ast::ExprFString> for StringLike<'a> { } } +impl<'a> From<&StringLike<'a>> for ExpressionRef<'a> { + fn from(value: &StringLike<'a>) -> Self { + match value { + StringLike::String(expr) => ExpressionRef::StringLiteral(expr), + StringLike::Bytes(expr) => ExpressionRef::BytesLiteral(expr), + StringLike::FString(expr) => ExpressionRef::FString(expr), + } + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(value: StringLike<'a>) -> Self { + AnyNodeRef::from(&value) + } +} + +impl<'a> From<&StringLike<'a>> for AnyNodeRef<'a> { + fn from(value: &StringLike<'a>) -> Self { + match value { + StringLike::String(expr) => AnyNodeRef::ExprStringLiteral(expr), + StringLike::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr), + StringLike::FString(expr) => AnyNodeRef::ExprFString(expr), + } + } +} + +impl<'a> TryFrom<&'a Expr> for StringLike<'a> { + type Error = (); + + fn try_from(value: &'a Expr) -> Result { + match value { + Expr::StringLiteral(value) => Ok(Self::String(value)), + Expr::BytesLiteral(value) => Ok(Self::Bytes(value)), + Expr::FString(value) => Ok(Self::FString(value)), + _ => Err(()), + } + } +} + impl Ranged for StringLike<'_> { fn range(&self) -> TextRange { match self { @@ -460,6 +515,15 @@ impl StringLikePart<'_> { StringLikePart::FString(f_string) => AnyStringFlags::from(f_string.flags), } } + + /// Returns the range of the string's content in the source (minus prefix and quotes). + pub fn content_range(self) -> TextRange { + let kind = self.flags(); + TextRange::new( + self.start() + kind.opener_len(), + self.end() - kind.closer_len(), + ) + } } impl<'a> From<&'a ast::StringLiteral> for StringLikePart<'a> { @@ -480,6 +544,16 @@ impl<'a> From<&'a ast::FString> for StringLikePart<'a> { } } +impl<'a> From<&StringLikePart<'a>> for AnyNodeRef<'a> { + fn from(value: &StringLikePart<'a>) -> Self { + match value { + StringLikePart::String(part) => AnyNodeRef::StringLiteral(part), + StringLikePart::Bytes(part) => AnyNodeRef::BytesLiteral(part), + StringLikePart::FString(part) => AnyNodeRef::FString(part), + } + } +} + impl Ranged for StringLikePart<'_> { fn range(&self) -> TextRange { match self { diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index 95cb9a3c3a..46b5e15bc5 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -5,7 +5,7 @@ use smallvec::SmallVec; use ruff_formatter::write; use ruff_python_ast::{ - Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprCompare, ExprUnaryOp, UnaryOp, + Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprCompare, ExprUnaryOp, StringLike, UnaryOp, }; use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; @@ -20,7 +20,7 @@ use crate::expression::parentheses::{ }; use crate::expression::OperatorPrecedence; use crate::prelude::*; -use crate::string::{AnyString, FormatImplicitConcatenatedString}; +use crate::string::FormatImplicitConcatenatedString; #[derive(Copy, Clone, Debug)] pub(super) enum BinaryLike<'a> { @@ -293,7 +293,8 @@ impl Format> for BinaryLike<'_> { let mut string_operands = flat_binary .operands() .filter_map(|(index, operand)| { - AnyString::from_expression(operand.expression()) + StringLike::try_from(operand.expression()) + .ok() .filter(|string| { string.is_implicit_concatenated() && !is_expression_parenthesized( diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index f93f12df7f..15b2b32814 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,11 +1,11 @@ -use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprBinOp; +use ruff_python_ast::{AnyNodeRef, StringLike}; use crate::expression::binary_like::BinaryLike; use crate::expression::has_parentheses; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; -use crate::string::AnyString; +use crate::string::StringLikeExtensions; #[derive(Default)] pub struct FormatExprBinOp; @@ -25,7 +25,7 @@ impl NeedsParentheses for ExprBinOp { ) -> OptionalParentheses { if parent.is_expr_await() { OptionalParentheses::Always - } else if let Some(string) = AnyString::from_expression(&self.left) { + } else if let Ok(string) = StringLike::try_from(&*self.left) { // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses if !string.is_implicit_concatenated() && string.is_multiline(context.source()) diff --git a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs index 132f08b9d6..7b6837b655 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs @@ -1,11 +1,11 @@ -use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprBytesLiteral; +use ruff_python_ast::{AnyNodeRef, StringLike}; use crate::expression::parentheses::{ in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; use crate::prelude::*; -use crate::string::{AnyString, FormatImplicitConcatenatedString}; +use crate::string::{FormatImplicitConcatenatedString, StringLikeExtensions}; #[derive(Default)] pub struct FormatExprBytesLiteral; @@ -29,7 +29,7 @@ impl NeedsParentheses for ExprBytesLiteral { ) -> OptionalParentheses { if self.value.is_implicit_concatenated() { OptionalParentheses::Multiline - } else if AnyString::Bytes(self).is_multiline(context.source()) { + } else if StringLike::Bytes(self).is_multiline(context.source()) { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 15c3cfb932..13f072cde5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,12 +1,12 @@ use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule}; -use ruff_python_ast::AnyNodeRef; +use ruff_python_ast::{AnyNodeRef, StringLike}; use ruff_python_ast::{CmpOp, ExprCompare}; use crate::expression::binary_like::BinaryLike; use crate::expression::has_parentheses; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; -use crate::string::AnyString; +use crate::string::StringLikeExtensions; #[derive(Default)] pub struct FormatExprCompare; @@ -26,7 +26,7 @@ impl NeedsParentheses for ExprCompare { ) -> OptionalParentheses { if parent.is_expr_await() { OptionalParentheses::Always - } else if let Some(string) = AnyString::from_expression(&self.left) { + } else if let Ok(string) = StringLike::try_from(&*self.left) { // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses if !string.is_implicit_concatenated() && string.is_multiline(context.source()) diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index 1690eca9a3..e88638d7c2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::{AnyNodeRef, ExprFString}; +use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike}; use ruff_source_file::Locator; use ruff_text_size::Ranged; @@ -7,7 +7,7 @@ use crate::expression::parentheses::{ }; use crate::other::f_string_part::FormatFStringPart; use crate::prelude::*; -use crate::string::{AnyString, FormatImplicitConcatenatedString, Quoting}; +use crate::string::{FormatImplicitConcatenatedString, Quoting, StringLikeExtensions}; #[derive(Default)] pub struct FormatExprFString; @@ -53,7 +53,7 @@ impl NeedsParentheses for ExprFString { // ``` // This isn't decided yet, refer to the relevant discussion: // https://github.com/astral-sh/ruff/discussions/9785 - } else if AnyString::FString(self).is_multiline(context.source()) { + } else if StringLike::FString(self).is_multiline(context.source()) { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs index 23e8eab633..2ee661f76d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs @@ -1,12 +1,12 @@ use ruff_formatter::FormatRuleWithOptions; -use ruff_python_ast::{AnyNodeRef, ExprStringLiteral}; +use ruff_python_ast::{AnyNodeRef, ExprStringLiteral, StringLike}; use crate::expression::parentheses::{ in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; use crate::other::string_literal::StringLiteralKind; use crate::prelude::*; -use crate::string::{AnyString, FormatImplicitConcatenatedString}; +use crate::string::{FormatImplicitConcatenatedString, StringLikeExtensions}; #[derive(Default)] pub struct FormatExprStringLiteral { @@ -48,7 +48,7 @@ impl NeedsParentheses for ExprStringLiteral { ) -> OptionalParentheses { if self.value.is_implicit_concatenated() { OptionalParentheses::Multiline - } else if AnyString::String(self).is_multiline(context.source()) { + } else if StringLike::String(self).is_multiline(context.source()) { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 438989fdb7..6e50f1cf01 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,5 +1,5 @@ use ruff_formatter::{write, FormatContext}; -use ruff_python_ast::{ArgOrKeyword, Arguments, Expr}; +use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, StringLike}; use ruff_python_trivia::{PythonWhitespace, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -8,7 +8,7 @@ use crate::expression::is_expression_huggable; use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses}; use crate::other::commas; use crate::prelude::*; -use crate::string::AnyString; +use crate::string::StringLikeExtensions; #[derive(Default)] pub struct FormatArguments; @@ -179,8 +179,8 @@ fn is_arguments_huggable(arguments: &Arguments, context: &PyFormatContext) -> bo // If the expression itself isn't huggable, then we can't hug it. if !(is_expression_huggable(arg, context) - || AnyString::from_expression(arg) - .is_some_and(|string| is_huggable_string_argument(string, arguments, context))) + || StringLike::try_from(arg) + .is_ok_and(|string| is_huggable_string_argument(string, arguments, context))) { return false; } @@ -219,7 +219,7 @@ fn is_arguments_huggable(arguments: &Arguments, context: &PyFormatContext) -> bo /// ) /// ``` fn is_huggable_string_argument( - string: AnyString, + string: StringLike, arguments: &Arguments, context: &PyFormatContext, ) -> bool { diff --git a/crates/ruff_python_formatter/src/string/any.rs b/crates/ruff_python_formatter/src/string/any.rs deleted file mode 100644 index 50bab2ce04..0000000000 --- a/crates/ruff_python_formatter/src/string/any.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::iter::FusedIterator; - -use memchr::memchr2; - -use ruff_python_ast::{ - self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString, - ExprStringLiteral, ExpressionRef, StringFlags, StringLiteral, -}; -use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange}; - -use crate::expression::expr_f_string::f_string_quoting; -use crate::string::Quoting; - -/// Represents any kind of string expression. This could be either a string, -/// bytes or f-string. -#[derive(Copy, Clone, Debug)] -pub(crate) enum AnyString<'a> { - String(&'a ExprStringLiteral), - Bytes(&'a ExprBytesLiteral), - FString(&'a ExprFString), -} - -impl<'a> AnyString<'a> { - /// Creates a new [`AnyString`] from the given [`Expr`]. - /// - /// Returns `None` if the expression is not either a string, bytes or f-string. - pub(crate) fn from_expression(expression: &'a Expr) -> Option> { - match expression { - Expr::StringLiteral(string) => Some(AnyString::String(string)), - Expr::BytesLiteral(bytes) => Some(AnyString::Bytes(bytes)), - Expr::FString(fstring) => Some(AnyString::FString(fstring)), - _ => None, - } - } - - /// Returns `true` if the string is implicitly concatenated. - pub(crate) fn is_implicit_concatenated(self) -> bool { - match self { - Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(), - Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(), - Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(), - } - } - - pub(crate) const fn is_fstring(self) -> bool { - matches!(self, Self::FString(_)) - } - - /// Returns the quoting to be used for this string. - pub(super) fn quoting(self, locator: &Locator<'_>) -> Quoting { - match self { - Self::String(_) | Self::Bytes(_) => Quoting::CanChange, - Self::FString(f_string) => f_string_quoting(f_string, locator), - } - } - - /// Returns an iterator over the [`AnyStringPart`]s of this string. - pub(super) fn parts(self) -> AnyStringPartsIter<'a> { - match self { - Self::String(ExprStringLiteral { value, .. }) => { - AnyStringPartsIter::String(value.iter()) - } - Self::Bytes(ExprBytesLiteral { value, .. }) => AnyStringPartsIter::Bytes(value.iter()), - Self::FString(ExprFString { value, .. }) => AnyStringPartsIter::FString(value.iter()), - } - } - - pub(crate) fn is_multiline(self, source: &str) -> bool { - match self { - AnyString::String(_) | AnyString::Bytes(_) => { - self.parts() - .next() - .is_some_and(|part| part.flags().is_triple_quoted()) - && memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some() - } - AnyString::FString(fstring) => { - memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some() - } - } - } -} - -impl Ranged for AnyString<'_> { - fn range(&self) -> TextRange { - match self { - Self::String(expr) => expr.range(), - Self::Bytes(expr) => expr.range(), - Self::FString(expr) => expr.range(), - } - } -} - -impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> { - fn from(value: &AnyString<'a>) -> Self { - match value { - AnyString::String(expr) => AnyNodeRef::ExprStringLiteral(expr), - AnyString::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr), - AnyString::FString(expr) => AnyNodeRef::ExprFString(expr), - } - } -} - -impl<'a> From> for AnyNodeRef<'a> { - fn from(value: AnyString<'a>) -> Self { - AnyNodeRef::from(&value) - } -} - -impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> { - fn from(value: &AnyString<'a>) -> Self { - match value { - AnyString::String(expr) => ExpressionRef::StringLiteral(expr), - AnyString::Bytes(expr) => ExpressionRef::BytesLiteral(expr), - AnyString::FString(expr) => ExpressionRef::FString(expr), - } - } -} - -impl<'a> From<&'a ExprBytesLiteral> for AnyString<'a> { - fn from(value: &'a ExprBytesLiteral) -> Self { - AnyString::Bytes(value) - } -} - -impl<'a> From<&'a ExprStringLiteral> for AnyString<'a> { - fn from(value: &'a ExprStringLiteral) -> Self { - AnyString::String(value) - } -} - -impl<'a> From<&'a ExprFString> for AnyString<'a> { - fn from(value: &'a ExprFString) -> Self { - AnyString::FString(value) - } -} - -pub(super) enum AnyStringPartsIter<'a> { - String(std::slice::Iter<'a, StringLiteral>), - Bytes(std::slice::Iter<'a, ast::BytesLiteral>), - FString(std::slice::Iter<'a, ast::FStringPart>), -} - -impl<'a> Iterator for AnyStringPartsIter<'a> { - type Item = AnyStringPart<'a>; - - fn next(&mut self) -> Option { - let part = match self { - Self::String(inner) => AnyStringPart::String(inner.next()?), - Self::Bytes(inner) => AnyStringPart::Bytes(inner.next()?), - Self::FString(inner) => match inner.next()? { - ast::FStringPart::Literal(string_literal) => AnyStringPart::String(string_literal), - ast::FStringPart::FString(f_string) => AnyStringPart::FString(f_string), - }, - }; - - Some(part) - } -} - -impl FusedIterator for AnyStringPartsIter<'_> {} - -/// Represents any kind of string which is part of an implicitly concatenated -/// string. This could be either a string, bytes or f-string. -/// -/// This is constructed from the [`AnyString::parts`] method on [`AnyString`]. -#[derive(Clone, Debug)] -pub(super) enum AnyStringPart<'a> { - String(&'a ast::StringLiteral), - Bytes(&'a ast::BytesLiteral), - FString(&'a ast::FString), -} - -impl AnyStringPart<'_> { - fn flags(&self) -> AnyStringFlags { - match self { - Self::String(part) => part.flags.into(), - Self::Bytes(bytes_literal) => bytes_literal.flags.into(), - Self::FString(part) => part.flags.into(), - } - } -} - -impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> { - fn from(value: &AnyStringPart<'a>) -> Self { - match value { - AnyStringPart::String(part) => AnyNodeRef::StringLiteral(part), - AnyStringPart::Bytes(part) => AnyNodeRef::BytesLiteral(part), - AnyStringPart::FString(part) => AnyNodeRef::FString(part), - } - } -} - -impl Ranged for AnyStringPart<'_> { - fn range(&self) -> TextRange { - match self { - Self::String(part) => part.range(), - Self::Bytes(part) => part.range(), - Self::FString(part) => part.range(), - } - } -} diff --git a/crates/ruff_python_formatter/src/string/mod.rs b/crates/ruff_python_formatter/src/string/mod.rs index 40a218d018..3eaf87121f 100644 --- a/crates/ruff_python_formatter/src/string/mod.rs +++ b/crates/ruff_python_formatter/src/string/mod.rs @@ -1,23 +1,24 @@ -pub(crate) use any::AnyString; +use memchr::memchr2; + pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer}; use ruff_formatter::format_args; use ruff_python_ast::str::Quote; use ruff_python_ast::{ self as ast, str_prefix::{AnyStringPrefix, StringLiteralPrefix}, - AnyStringFlags, StringFlags, + AnyStringFlags, StringFlags, StringLike, StringLikePart, }; -use ruff_text_size::{Ranged, TextRange}; +use ruff_source_file::Locator; +use ruff_text_size::Ranged; use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::expr_f_string::f_string_quoting; use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space; use crate::other::f_string::FormatFString; use crate::other::string_literal::StringLiteralKind; use crate::prelude::*; -use crate::string::any::AnyStringPart; use crate::QuoteStyle; -mod any; pub(crate) mod docstring; mod normalize; @@ -31,11 +32,11 @@ pub(crate) enum Quoting { /// Formats any implicitly concatenated string. This could be any valid combination /// of string, bytes or f-string literals. pub(crate) struct FormatImplicitConcatenatedString<'a> { - string: AnyString<'a>, + string: StringLike<'a>, } impl<'a> FormatImplicitConcatenatedString<'a> { - pub(crate) fn new(string: impl Into>) -> Self { + pub(crate) fn new(string: impl Into>) -> Self { Self { string: string.into(), } @@ -53,7 +54,7 @@ impl Format> for FormatImplicitConcatenatedString<'_> { let part_comments = comments.leading_dangling_trailing(&part); let format_part = format_with(|f: &mut PyFormatter| match part { - AnyStringPart::String(part) => { + StringLikePart::String(part) => { let kind = if self.string.is_fstring() { #[allow(deprecated)] StringLiteralKind::InImplicitlyConcatenatedFString(quoting) @@ -63,8 +64,8 @@ impl Format> for FormatImplicitConcatenatedString<'_> { part.format().with_options(kind).fmt(f) } - AnyStringPart::Bytes(bytes_literal) => bytes_literal.format().fmt(f), - AnyStringPart::FString(part) => FormatFString::new(part, quoting).fmt(f), + StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f), + StringLikePart::FString(part) => FormatFString::new(part, quoting).fmt(f), }); joiner.entry(&format_args![ @@ -141,57 +142,32 @@ impl From for QuoteStyle { } } -#[derive(Debug, Clone, Copy)] -pub(crate) struct StringPart { - flags: AnyStringFlags, - range: TextRange, +// Extension trait that adds formatter specific helper methods to `StringLike`. +pub(crate) trait StringLikeExtensions { + fn quoting(&self, locator: &Locator<'_>) -> Quoting; + + fn is_multiline(&self, source: &str) -> bool; } -impl Ranged for StringPart { - fn range(&self) -> TextRange { - self.range - } -} - -impl StringPart { - /// Use the `kind()` method to retrieve information about the - fn flags(self) -> AnyStringFlags { - self.flags +impl StringLikeExtensions for ast::StringLike<'_> { + fn quoting(&self, locator: &Locator<'_>) -> Quoting { + match self { + Self::String(_) | Self::Bytes(_) => Quoting::CanChange, + Self::FString(f_string) => f_string_quoting(f_string, locator), + } } - /// Returns the range of the string's content in the source (minus prefix and quotes). - fn content_range(self) -> TextRange { - let kind = self.flags(); - TextRange::new( - self.start() + kind.opener_len(), - self.end() - kind.closer_len(), - ) - } -} - -impl From<&ast::StringLiteral> for StringPart { - fn from(value: &ast::StringLiteral) -> Self { - Self { - range: value.range, - flags: value.flags.into(), - } - } -} - -impl From<&ast::BytesLiteral> for StringPart { - fn from(value: &ast::BytesLiteral) -> Self { - Self { - range: value.range, - flags: value.flags.into(), - } - } -} - -impl From<&ast::FString> for StringPart { - fn from(value: &ast::FString) -> Self { - Self { - range: value.range, - flags: value.flags.into(), + fn is_multiline(&self, source: &str) -> bool { + match self { + Self::String(_) | Self::Bytes(_) => { + self.parts() + .next() + .is_some_and(|part| part.flags().is_triple_quoted()) + && memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some() + } + Self::FString(fstring) => { + memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some() + } } } } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index 2e95575db2..6836ad8082 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -3,13 +3,13 @@ use std::cmp::Ordering; use std::iter::FusedIterator; use ruff_formatter::FormatContext; -use ruff_python_ast::{str::Quote, AnyStringFlags, StringFlags}; +use ruff_python_ast::{str::Quote, AnyStringFlags, StringFlags, StringLikePart}; use ruff_text_size::{Ranged, TextRange}; use crate::context::FStringState; use crate::prelude::*; use crate::preview::is_f_string_formatting_enabled; -use crate::string::{Quoting, StringPart, StringQuotes}; +use crate::string::{Quoting, StringQuotes}; use crate::QuoteStyle; pub(crate) struct StringNormalizer<'a, 'src> { @@ -37,7 +37,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { self } - fn quoting(&self, string: StringPart) -> Quoting { + fn quoting(&self, string: StringLikePart) -> Quoting { if let FStringState::InsideExpressionElement(context) = self.context.f_string_state() { // If we're inside an f-string, we need to make sure to preserve the // existing quotes unless we're inside a triple-quoted f-string and @@ -66,7 +66,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { } /// Computes the strings preferred quotes. - pub(crate) fn choose_quotes(&self, string: StringPart) -> QuoteSelection { + pub(crate) fn choose_quotes(&self, string: StringLikePart) -> QuoteSelection { let raw_content = self.context.locator().slice(string.content_range()); let first_quote_or_normalized_char_offset = raw_content .bytes() @@ -168,7 +168,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { } /// Computes the strings preferred quotes and normalizes its content. - pub(crate) fn normalize(&self, string: StringPart) -> NormalizedString<'src> { + pub(crate) fn normalize(&self, string: StringLikePart) -> NormalizedString<'src> { let raw_content = self.context.locator().slice(string.content_range()); let quote_selection = self.choose_quotes(string);