mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:28 +00:00
Call chain formatting in fluent style (#6151)
Implement fluent style/call chains. See the `call_chains.py` formatting for examples. This isn't fully like black because in `raise A from B` they allow `A` breaking can influence the formatting of `B` even if it is already multiline. Similarity index: | project | main | PR | |--------------|-------|-------| | build | ??? | 0.753 | | django | 0.991 | 0.998 | | transformers | 0.993 | 0.994 | | typeshed | 0.723 | 0.723 | | warehouse | 0.978 | 0.994 | | zulip | 0.992 | 0.994 | Call chain formatting is affected by https://github.com/astral-sh/ruff/issues/627, but i'm cutting scope here. Closes #5343 **Test Plan**: * Added a dedicated call chains test file * The ecosystem checks found some bugs * I manually check django and zulip formatting --------- Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
35bdbe43a8
commit
99baad12d8
16 changed files with 917 additions and 517 deletions
|
@ -1,15 +1,28 @@
|
|||
use ruff_formatter::{write, FormatRuleWithOptions};
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant};
|
||||
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments};
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
|
||||
use crate::expression::parentheses::{
|
||||
is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
|
||||
};
|
||||
use crate::expression::CallChainLayout;
|
||||
use crate::prelude::*;
|
||||
use crate::FormatNodeRule;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprAttribute;
|
||||
pub struct FormatExprAttribute {
|
||||
call_chain_layout: CallChainLayout,
|
||||
}
|
||||
|
||||
impl FormatRuleWithOptions<ExprAttribute, PyFormatContext<'_>> for FormatExprAttribute {
|
||||
type Options = CallChainLayout;
|
||||
|
||||
fn with_options(mut self, options: Self::Options) -> Self {
|
||||
self.call_chain_layout = options;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
||||
fn fmt_fields(&self, item: &ExprAttribute, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
|
@ -20,6 +33,8 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
ctx: _,
|
||||
} = item;
|
||||
|
||||
let call_chain_layout = self.call_chain_layout.apply_in_node(item, f);
|
||||
|
||||
let needs_parentheses = matches!(
|
||||
value.as_ref(),
|
||||
Expr::Constant(ExprConstant {
|
||||
|
@ -37,11 +52,36 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
|
||||
if needs_parentheses {
|
||||
value.format().with_options(Parentheses::Always).fmt(f)?;
|
||||
} else if let Expr::Attribute(expr_attribute) = value.as_ref() {
|
||||
// We're in a attribute chain (`a.b.c`). The outermost node adds parentheses if
|
||||
// required, the inner ones don't need them so we skip the `Expr` formatting that
|
||||
// normally adds the parentheses.
|
||||
expr_attribute.format().fmt(f)?;
|
||||
} else if call_chain_layout == CallChainLayout::Fluent {
|
||||
match value.as_ref() {
|
||||
Expr::Attribute(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
}
|
||||
Expr::Call(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
if call_chain_layout == CallChainLayout::Fluent {
|
||||
// Format the dot on its own line
|
||||
soft_line_break().fmt(f)?;
|
||||
}
|
||||
}
|
||||
Expr::Subscript(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
if call_chain_layout == CallChainLayout::Fluent {
|
||||
// Format the dot on its own line
|
||||
soft_line_break().fmt(f)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// This matches [`CallChainLayout::from_expression`]
|
||||
if is_expression_parenthesized(value.as_ref().into(), f.context().source()) {
|
||||
value.format().with_options(Parentheses::Always).fmt(f)?;
|
||||
// Format the dot on its own line
|
||||
soft_line_break().fmt(f)?;
|
||||
} else {
|
||||
value.format().fmt(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value.format().fmt(f)?;
|
||||
}
|
||||
|
@ -50,16 +90,51 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
hard_line_break().fmt(f)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text("."),
|
||||
trailing_comments(trailing_dot_comments),
|
||||
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
|
||||
leading_comments(leading_attribute_comments),
|
||||
attr.format()
|
||||
]
|
||||
)
|
||||
if call_chain_layout == CallChainLayout::Fluent {
|
||||
// Fluent style has line breaks before the dot
|
||||
// ```python
|
||||
// blogs3 = (
|
||||
// Blog.objects.filter(
|
||||
// entry__headline__contains="Lennon",
|
||||
// )
|
||||
// .filter(
|
||||
// entry__pub_date__year=2008,
|
||||
// )
|
||||
// .filter(
|
||||
// entry__pub_date__year=2008,
|
||||
// )
|
||||
// )
|
||||
// ```
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
|
||||
leading_comments(leading_attribute_comments),
|
||||
text("."),
|
||||
trailing_comments(trailing_dot_comments),
|
||||
attr.format()
|
||||
]
|
||||
)
|
||||
} else {
|
||||
// Regular style
|
||||
// ```python
|
||||
// blogs2 = Blog.objects.filter(
|
||||
// entry__headline__contains="Lennon",
|
||||
// ).filter(
|
||||
// entry__pub_date__year=2008,
|
||||
// )
|
||||
// ```
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text("."),
|
||||
trailing_comments(trailing_dot_comments),
|
||||
(!leading_attribute_comments.is_empty()).then_some(hard_line_break()),
|
||||
leading_comments(leading_attribute_comments),
|
||||
attr.format()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
@ -79,7 +154,11 @@ impl NeedsParentheses for ExprAttribute {
|
|||
context: &PyFormatContext,
|
||||
) -> OptionalParentheses {
|
||||
// Checks if there are any own line comments in an attribute chain (a.b.c).
|
||||
if context
|
||||
if CallChainLayout::from_expression(self.into(), context.source())
|
||||
== CallChainLayout::Fluent
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if context
|
||||
.comments()
|
||||
.dangling_comments(self)
|
||||
.iter()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue