mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:15:12 +00:00

## 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.
84 lines
2.6 KiB
Rust
84 lines
2.6 KiB
Rust
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
|
|
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::StringLikeExtensions;
|
|
|
|
#[derive(Default)]
|
|
pub struct FormatExprCompare;
|
|
|
|
impl FormatNodeRule<ExprCompare> for FormatExprCompare {
|
|
#[inline]
|
|
fn fmt_fields(&self, item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> {
|
|
BinaryLike::Compare(item).fmt(f)
|
|
}
|
|
}
|
|
|
|
impl NeedsParentheses for ExprCompare {
|
|
fn needs_parentheses(
|
|
&self,
|
|
parent: AnyNodeRef,
|
|
context: &PyFormatContext,
|
|
) -> OptionalParentheses {
|
|
if parent.is_expr_await() {
|
|
OptionalParentheses::Always
|
|
} 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)
|
|
&& !context.comments().has(string)
|
|
&& self.comparators.first().is_some_and(|right| {
|
|
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
|
})
|
|
{
|
|
OptionalParentheses::Never
|
|
} else {
|
|
OptionalParentheses::Multiline
|
|
}
|
|
} else {
|
|
OptionalParentheses::Multiline
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub struct FormatCmpOp;
|
|
|
|
impl<'ast> AsFormat<PyFormatContext<'ast>> for CmpOp {
|
|
type Format<'a> = FormatRefWithRule<'a, CmpOp, FormatCmpOp, PyFormatContext<'ast>>;
|
|
|
|
fn format(&self) -> Self::Format<'_> {
|
|
FormatRefWithRule::new(self, FormatCmpOp)
|
|
}
|
|
}
|
|
|
|
impl<'ast> IntoFormat<PyFormatContext<'ast>> for CmpOp {
|
|
type Format = FormatOwnedWithRule<CmpOp, FormatCmpOp, PyFormatContext<'ast>>;
|
|
|
|
fn into_format(self) -> Self::Format {
|
|
FormatOwnedWithRule::new(self, FormatCmpOp)
|
|
}
|
|
}
|
|
|
|
impl FormatRule<CmpOp, PyFormatContext<'_>> for FormatCmpOp {
|
|
fn fmt(&self, item: &CmpOp, f: &mut PyFormatter) -> FormatResult<()> {
|
|
let operator = match item {
|
|
CmpOp::Eq => "==",
|
|
CmpOp::NotEq => "!=",
|
|
CmpOp::Lt => "<",
|
|
CmpOp::LtE => "<=",
|
|
CmpOp::Gt => ">",
|
|
CmpOp::GtE => ">=",
|
|
CmpOp::Is => "is",
|
|
CmpOp::IsNot => "is not",
|
|
CmpOp::In => "in",
|
|
CmpOp::NotIn => "not in",
|
|
};
|
|
|
|
token(operator).fmt(f)
|
|
}
|
|
}
|