Fix formatting of chained boolean operations (#6394)

Closes https://github.com/astral-sh/ruff/issues/6068

These commits are kind of a mess as I did some stumbling around here. 

Unrolls formatting of chained boolean operations to prevent nested
grouping which gives us Black-compatible formatting where each boolean
operation is on a new line.
This commit is contained in:
Zanie Blue 2023-08-07 12:22:33 -05:00 committed by GitHub
parent 63ffadf0b8
commit 999d88e773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 177 additions and 16 deletions

View file

@ -62,3 +62,35 @@ if (
and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] and [dddddddddddddd, eeeeeeeeee, fffffffffffffff]
): ):
pass pass
# Regression test for https://github.com/astral-sh/ruff/issues/6068
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or numpy and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and numpy or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy) and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy) or isinstance(ccccccccccc, dddddd)
):
pass

View file

@ -5,18 +5,27 @@ use crate::expression::parentheses::{
}; };
use crate::prelude::*; use crate::prelude::*;
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::{AnyNodeRef, AstNode};
use ruff_python_ast::{BoolOp, ExprBoolOp}; use ruff_python_ast::{BoolOp, Expr, ExprBoolOp};
use super::parentheses::is_expression_parenthesized;
#[derive(Default)] #[derive(Default)]
pub struct FormatExprBoolOp { pub struct FormatExprBoolOp {
parentheses: Option<Parentheses>, parentheses: Option<Parentheses>,
chained: bool,
}
pub struct BoolOpLayout {
pub(crate) parentheses: Option<Parentheses>,
pub(crate) chained: bool,
} }
impl FormatRuleWithOptions<ExprBoolOp, PyFormatContext<'_>> for FormatExprBoolOp { impl FormatRuleWithOptions<ExprBoolOp, PyFormatContext<'_>> for FormatExprBoolOp {
type Options = Option<Parentheses>; type Options = BoolOpLayout;
fn with_options(mut self, options: Self::Options) -> Self { fn with_options(mut self, options: Self::Options) -> Self {
self.parentheses = options; self.parentheses = options.parentheses;
self.chained = options.chained;
self self
} }
} }
@ -37,7 +46,7 @@ impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
return Ok(()); return Ok(());
}; };
write!(f, [in_parentheses_only_group(&first.format())])?; FormatValue { value: first }.fmt(f)?;
for value in values { for value in values {
let leading_value_comments = comments.leading_comments(value); let leading_value_comments = comments.leading_comments(value);
@ -51,20 +60,20 @@ impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
)?; )?;
} }
write!( write!(f, [op.format(), space(),])?;
f,
[ FormatValue { value }.fmt(f)?;
op.format(),
space(),
in_parentheses_only_group(&value.format())
]
)?;
} }
Ok(()) Ok(())
}); });
in_parentheses_only_group(&inner).fmt(f) if self.chained {
// Chained boolean operations should not be given a new group
inner.fmt(f)
} else {
in_parentheses_only_group(&inner).fmt(f)
}
} }
} }
@ -78,6 +87,33 @@ impl NeedsParentheses for ExprBoolOp {
} }
} }
struct FormatValue<'a> {
value: &'a Expr,
}
impl Format<PyFormatContext<'_>> for FormatValue<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
match self.value {
Expr::BoolOp(bool_op)
if !is_expression_parenthesized(
bool_op.as_any_node_ref(),
f.context().source(),
) =>
{
// Mark chained boolean operations e.g. `x and y or z` and avoid creating a new group
write!(
f,
[bool_op.format().with_options(BoolOpLayout {
parentheses: None,
chained: true,
})]
)
}
_ => write!(f, [in_parentheses_only_group(&self.value.format())]),
}
}
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct FormatBoolOp; pub struct FormatBoolOp;

View file

@ -16,6 +16,8 @@ use crate::expression::parentheses::{
}; };
use crate::prelude::*; use crate::prelude::*;
use self::expr_bool_op::BoolOpLayout;
pub(crate) mod expr_attribute; pub(crate) mod expr_attribute;
pub(crate) mod expr_await; pub(crate) mod expr_await;
pub(crate) mod expr_bin_op; pub(crate) mod expr_bin_op;
@ -67,7 +69,13 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
let parentheses = self.parentheses; let parentheses = self.parentheses;
let format_expr = format_with(|f| match expression { let format_expr = format_with(|f| match expression {
Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::BoolOp(expr) => expr
.format()
.with_options(BoolOpLayout {
parentheses: Some(parentheses),
chained: false,
})
.fmt(f),
Expr::NamedExpr(expr) => expr.format().fmt(f), Expr::NamedExpr(expr) => expr.format().fmt(f),
Expr::BinOp(expr) => expr.format().fmt(f), Expr::BinOp(expr) => expr.format().fmt(f),
Expr::UnaryOp(expr) => expr.format().fmt(f), Expr::UnaryOp(expr) => expr.format().fmt(f),

View file

@ -180,7 +180,8 @@ def test():
", %s unmodified" % unmodified_count if collected["unmodified"] else "" ", %s unmodified" % unmodified_count if collected["unmodified"] else ""
), ),
"post_processed": ( "post_processed": (
collected["post_processed"] and ", %s post-processed" % post_processed_count collected["post_processed"]
and ", %s post-processed" % post_processed_count
or "" or ""
), ),
} }

View file

@ -68,6 +68,38 @@ if (
and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] and [dddddddddddddd, eeeeeeeeee, fffffffffffffff]
): ):
pass pass
# Regression test for https://github.com/astral-sh/ruff/issues/6068
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or numpy and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and numpy or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) or (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy) and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb) and (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy) or isinstance(ccccccccccc, dddddd)
):
pass
``` ```
## Output ## Output
@ -136,6 +168,58 @@ if (
and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] and [dddddddddddddd, eeeeeeeeee, fffffffffffffff]
): ):
pass pass
# Regression test for https://github.com/astral-sh/ruff/issues/6068
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
or numpy
and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
and numpy
or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
or xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
and xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
or isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
or (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
)
and isinstance(ccccccccccc, dddddd)
):
pass
if not (
isinstance(aaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbb)
and (
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
)
or isinstance(ccccccccccc, dddddd)
):
pass
``` ```