mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-12 22:58:22 +00:00
Fix f-string formatting in assignment statement (#14454)
## Summary fixes: #13813 This PR fixes a bug in the formatting assignment statement when the value is an f-string. This is resolved by using custom best fit layouts if the f-string is (a) not already a flat f-string (thus, cannot be multiline) and (b) is not a multiline string (thus, cannot be flattened). So, it is used in cases like the following: ```py aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ expression}moreeeeeeeeeeeeeeeee" ``` Which is (a) `FStringLayout::Multiline` and (b) not a multiline. There are various other examples in the PR diff along with additional explanation and context as code comments. ## Test Plan Add multiple test cases for various scenarios.
This commit is contained in:
parent
e4cefd9bf9
commit
f3dac27e9a
15 changed files with 2184 additions and 74 deletions
|
@ -154,7 +154,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
|||
}
|
||||
|
||||
// Multiline strings can never fit on a single line.
|
||||
if !string.is_fstring() && string.is_multiline(context.source()) {
|
||||
if string.is_multiline(context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -187,25 +187,6 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
|||
}
|
||||
|
||||
if let StringLikePart::FString(fstring) = part {
|
||||
if fstring.elements.iter().any(|element| match element {
|
||||
// Same as for other literals. Multiline literals can't fit on a single line.
|
||||
FStringElement::Literal(literal) => {
|
||||
context.source().contains_line_break(literal.range())
|
||||
}
|
||||
FStringElement::Expression(expression) => {
|
||||
if is_f_string_formatting_enabled(context) {
|
||||
// Expressions containing comments can't be joined.
|
||||
context.comments().contains_comments(expression.into())
|
||||
} else {
|
||||
// Multiline f-string expressions can't be joined if the f-string formatting is disabled because
|
||||
// the string gets inserted in verbatim preserving the newlines.
|
||||
context.source().contains_line_break(expression.range())
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if context.options().target_version().supports_pep_701() {
|
||||
if is_fstring_with_quoted_format_spec_and_debug(fstring, context) {
|
||||
if preserve_quotes_requirement
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
use memchr::memchr2;
|
||||
|
||||
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::StringLikePart;
|
||||
use ruff_python_ast::{
|
||||
self as ast,
|
||||
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
|
||||
AnyStringFlags, StringFlags,
|
||||
};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) mod docstring;
|
||||
|
@ -90,7 +92,7 @@ impl From<Quote> for QuoteStyle {
|
|||
pub(crate) trait StringLikeExtensions {
|
||||
fn quoting(&self, source: &str) -> Quoting;
|
||||
|
||||
fn is_multiline(&self, source: &str) -> bool;
|
||||
fn is_multiline(&self, context: &PyFormatContext) -> bool;
|
||||
}
|
||||
|
||||
impl StringLikeExtensions for ast::StringLike<'_> {
|
||||
|
@ -101,15 +103,59 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_multiline(&self, source: &str) -> bool {
|
||||
match self {
|
||||
Self::String(_) | Self::Bytes(_) => self.parts().any(|part| {
|
||||
fn is_multiline(&self, context: &PyFormatContext) -> bool {
|
||||
self.parts().any(|part| match part {
|
||||
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||
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()
|
||||
&& context.source().contains_line_break(part.range())
|
||||
}
|
||||
}
|
||||
StringLikePart::FString(f_string) => {
|
||||
fn contains_line_break_or_comments(
|
||||
elements: &ast::FStringElements,
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
elements.iter().any(|element| match element {
|
||||
ast::FStringElement::Literal(literal) => {
|
||||
context.source().contains_line_break(literal.range())
|
||||
}
|
||||
ast::FStringElement::Expression(expression) => {
|
||||
if is_f_string_formatting_enabled(context) {
|
||||
// Expressions containing comments can't be joined.
|
||||
//
|
||||
// Format specifiers needs to be checked as well. For example, the
|
||||
// following should be considered multiline because the literal
|
||||
// part of the format specifier contains a newline at the end
|
||||
// (`.3f\n`):
|
||||
//
|
||||
// ```py
|
||||
// x = f"hello {a + b + c + d:.3f
|
||||
// } world"
|
||||
// ```
|
||||
context.comments().contains_comments(expression.into())
|
||||
|| expression.format_spec.as_deref().is_some_and(|spec| {
|
||||
contains_line_break_or_comments(&spec.elements, context)
|
||||
})
|
||||
|| expression.debug_text.as_ref().is_some_and(|debug_text| {
|
||||
memchr2(b'\n', b'\r', debug_text.leading.as_bytes())
|
||||
.is_some()
|
||||
|| memchr2(b'\n', b'\r', debug_text.trailing.as_bytes())
|
||||
.is_some()
|
||||
})
|
||||
} else {
|
||||
// Multiline f-string expressions can't be joined if the f-string
|
||||
// formatting is disabled because the string gets inserted in
|
||||
// verbatim preserving the newlines.
|
||||
//
|
||||
// We don't need to check format specifiers or debug text here
|
||||
// because the expression range already includes them.
|
||||
context.source().contains_line_break(expression.range())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
contains_line_break_or_comments(&f_string.elements, context)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue