mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 12:55:05 +00:00
Avoid multiline expression if format specifier is present (#11123)
## Summary This PR fixes the bug where the formatter would format an f-string and could potentially change the AST. For a triple-quoted f-string, the element can't be formatted into multiline if it has a format specifier because otherwise the newline would be treated as part of the format specifier. Given the following f-string: ```python f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { variable:.3f} ddddddddddddddd eeeeeeee""" ``` The formatter sees that the f-string is already multiline so it assumes that it can contain line breaks i.e., broken into multiple lines. But, in this specific case we can't format it as: ```python f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { variable:.3f } ddddddddddddddd eeeeeeee""" ``` Because the format specifier string would become ".3f\n", which is not the original string (`.3f`). If the original source code already contained a newline, they'll be preserved. For example: ```python f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { variable:.3f } ddddddddddddddd eeeeeeee""" ``` The above will be formatted as: ```py f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f } ddddddddddddddd eeeeeeee""" ``` Note that the newline after `.3f` is part of the format specifier which needs to be preserved. The Python version is irrelevant in this case. fixes: #10040 ## Test Plan Add some test cases to verify this behavior.
This commit is contained in:
parent
16a1f3cbcc
commit
77a72ecd38
7 changed files with 220 additions and 31 deletions
|
@ -135,7 +135,7 @@ impl FStringLayout {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_flat(self) -> bool {
|
||||
matches!(self, Self::Flat)
|
||||
pub(crate) const fn is_multiline(self) -> bool {
|
||||
matches!(self, FStringLayout::Multiline)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,15 +64,63 @@ impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Context representing an f-string expression element.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct FStringExpressionElementContext {
|
||||
/// The context of the parent f-string containing this expression element.
|
||||
parent_context: FStringContext,
|
||||
/// Indicates whether this expression element has format specifier or not.
|
||||
has_format_spec: bool,
|
||||
}
|
||||
|
||||
impl FStringExpressionElementContext {
|
||||
/// Returns the [`FStringContext`] containing this expression element.
|
||||
pub(crate) fn f_string(self) -> FStringContext {
|
||||
self.parent_context
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression element can contain line breaks.
|
||||
pub(crate) fn can_contain_line_breaks(self) -> bool {
|
||||
self.parent_context.layout().is_multiline()
|
||||
// For a triple-quoted f-string, the element can't be formatted into multiline if it
|
||||
// has a format specifier because otherwise the newline would be treated as part of the
|
||||
// format specifier.
|
||||
//
|
||||
// Given the following f-string:
|
||||
// ```python
|
||||
// f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee"""
|
||||
// ```
|
||||
//
|
||||
// We can't format it as:
|
||||
// ```python
|
||||
// f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
|
||||
// variable:.3f
|
||||
// } ddddddddddddddd eeeeeeee"""
|
||||
// ```
|
||||
//
|
||||
// Here, the format specifier string would become ".3f\n", which is not what we want.
|
||||
// But, if the original source code already contained a newline, they'll be preserved.
|
||||
//
|
||||
// The Python version is irrelevant in this case.
|
||||
&& !(self.parent_context.kind().is_triple_quoted() && self.has_format_spec)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an f-string expression element.
|
||||
pub(crate) struct FormatFStringExpressionElement<'a> {
|
||||
element: &'a FStringExpressionElement,
|
||||
context: FStringContext,
|
||||
context: FStringExpressionElementContext,
|
||||
}
|
||||
|
||||
impl<'a> FormatFStringExpressionElement<'a> {
|
||||
pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self {
|
||||
Self { element, context }
|
||||
Self {
|
||||
element,
|
||||
context: FStringExpressionElementContext {
|
||||
parent_context: context,
|
||||
has_format_spec: element.format_spec.is_some(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,10 +201,10 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
|||
// added to maintain consistency.
|
||||
Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) => {
|
||||
Some(format_with(|f| {
|
||||
if self.context.layout().is_flat() {
|
||||
space().fmt(f)
|
||||
} else {
|
||||
if self.context.can_contain_line_breaks() {
|
||||
soft_line_break_or_space().fmt(f)
|
||||
} else {
|
||||
space().fmt(f)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -183,12 +231,9 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
|||
token(":").fmt(f)?;
|
||||
|
||||
f.join()
|
||||
.entries(
|
||||
format_spec
|
||||
.elements
|
||||
.iter()
|
||||
.map(|element| FormatFStringElement::new(element, self.context)),
|
||||
)
|
||||
.entries(format_spec.elements.iter().map(|element| {
|
||||
FormatFStringElement::new(element, self.context.f_string())
|
||||
}))
|
||||
.finish()?;
|
||||
|
||||
// These trailing comments can only occur if the format specifier is
|
||||
|
@ -205,7 +250,11 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
|||
trailing_comments(comments.trailing(self.element)).fmt(f)?;
|
||||
}
|
||||
|
||||
bracket_spacing.fmt(f)
|
||||
if conversion.is_none() && format_spec.is_none() {
|
||||
bracket_spacing.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let open_parenthesis_comments = if dangling_item_comments.is_empty() {
|
||||
|
@ -219,16 +268,16 @@ impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
|||
{
|
||||
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
|
||||
|
||||
if self.context.layout().is_flat() {
|
||||
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||
|
||||
write!(buffer, [open_parenthesis_comments, item])?;
|
||||
} else {
|
||||
if self.context.can_contain_line_breaks() {
|
||||
group(&format_args![
|
||||
open_parenthesis_comments,
|
||||
soft_block_indent(&item)
|
||||
])
|
||||
.fmt(&mut f)?;
|
||||
} else {
|
||||
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||
|
||||
write!(buffer, [open_parenthesis_comments, item])?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue